diff --git a/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java b/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java index ae1433d2d1..262793e071 100644 --- a/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java +++ b/worldedit-core/src/main/java/com/sk89q/util/StringUtil.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Arrays; import java.util.regex.Pattern; /** @@ -340,30 +341,36 @@ public static List parseListInQuotes(String[] input, char delimiter, cha if (quotes != quoteClose.length) { throw new Error("Mismatched quoteOpen and quoteClose lengths"); } + + int[] nestingDepths = new int[quotes]; for (String split : input) { - boolean quoteHandled = false; - for (int i = 0; i < quotes; i++) { - if (split.indexOf(quoteOpen[i]) != -1 && split.indexOf(quoteClose[i]) == -1) { - buffer.append(split).append(delimiter); - quoteHandled = true; - break; - } else if (split.indexOf(quoteClose[i]) != -1 && split.indexOf(quoteOpen[i]) == -1) { - buffer.append(split); - parsableBlocks.add(buffer.toString()); - buffer = new StringBuilder(); - quoteHandled = true; - break; - } - } - if (!quoteHandled) { - if (buffer.length() == 0) { - parsableBlocks.add(split); - } else { - buffer.append(split).append(delimiter); - } + split.chars() + .forEach(ch -> { + for (int i = 0; i < quoteOpen.length; i++) { + char openQuote = quoteOpen[i]; + if (openQuote == ch) { + nestingDepths[i]++; + } + } + for (int i = 0; i < quoteClose.length; i++) { + char closeQuote = quoteClose[i]; + if (closeQuote == ch) { + nestingDepths[i]--; + } + } + }); + + if (Arrays.stream(nestingDepths).allMatch(i -> i == 0)) { + //all quotes closed after this split + buffer.append(split); + parsableBlocks.add(buffer.toString()); + buffer = new StringBuilder(); + } else { + //ongoing quoting + buffer.append(split).append(delimiter); } } - if (appendLeftover && buffer.length() != 0) { + if (appendLeftover && !buffer.isEmpty()) { parsableBlocks.add(buffer.delete(buffer.length() - 1, buffer.length()).toString()); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java index 1b69eea628..148b209370 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/PatternFactory.java @@ -25,7 +25,7 @@ import com.sk89q.worldedit.extension.factory.parser.pattern.RandomPatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.RandomStatePatternParser; import com.sk89q.worldedit.extension.factory.parser.pattern.SingleBlockPatternParser; -import com.sk89q.worldedit.extension.factory.parser.pattern.TypeOrStateApplyingPatternParser; +import com.sk89q.worldedit.extension.factory.parser.pattern.PartiallyApplyingPatternParser; import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.internal.registry.AbstractFactory; @@ -51,7 +51,7 @@ public PatternFactory(WorldEdit worldEdit) { // individual patterns register(new ClipboardPatternParser(worldEdit)); - register(new TypeOrStateApplyingPatternParser(worldEdit)); + register(new PartiallyApplyingPatternParser(worldEdit)); register(new RandomStatePatternParser(worldEdit)); register(new BlockCategoryPatternParser(worldEdit)); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParser.java new file mode 100644 index 0000000000..1c3186f2f6 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParser.java @@ -0,0 +1,259 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extension.factory.parser.pattern; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.NoMatchException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.buffer.ExtentBuffer; +import com.sk89q.worldedit.function.pattern.*; +import com.sk89q.worldedit.internal.registry.InputParser; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; +import org.enginehub.linbus.format.snbt.LinStringIO; +import org.enginehub.linbus.stream.exception.NbtParseException; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + + +public class PartiallyApplyingPatternParser extends InputParser { + + boolean compatibilityMode = false; + + public PartiallyApplyingPatternParser(WorldEdit worldEdit) { + super(worldEdit); + } + + protected PartiallyApplyingPatternParser(WorldEdit worldEdit, boolean compatibilityMode) { + super(worldEdit); + this.compatibilityMode = compatibilityMode; + } + + @Override + public Stream getSuggestions(String input, ParserContext context) { + if (input.isEmpty()) { + return Stream.of("^"); + } + if (!input.startsWith("^")) { + return Stream.empty(); + } + input = input.substring(1); + + if (input.isEmpty()) { + //define properties, nbt or a type + return Stream.concat( + Stream.of("^[", "^{", "^{,"), + worldEdit.getPatternFactory().getSuggestions(input, context) + .stream() + .map(s -> "^" + s) + ); + } + + PartiallyApplyingComponents components = split(input); + + if (!components.nbt().isEmpty()) { + if (!components.type().isEmpty() && !components.properties().isEmpty()) { + //all of them are defined, we suggest like we would without ^ + return worldEdit.getPatternFactory().getSuggestions(input, context) + .stream() + .map(s -> "^" + s); + } + if (!components.type().isEmpty()) { + //type and nbt. We currently don't support nbt hints, so nothing to suggest + return Stream.empty(); + } + if (!components.properties().isEmpty()) { + //properties and nbt. We can't figure out possible nbt without type + return Stream.empty(); + } + } + + if (!components.properties().isEmpty()) { + if (!components.type().isEmpty()) { + //type and properties are defined, we suggest like we would without ^ + return worldEdit.getPatternFactory().getSuggestions(input, context) + .stream() + .map(s -> "^" + s); + } + return Stream.empty(); // without knowing a type, we can't really suggest states + } + //only type is defined, we suggest like we would without ^ + return worldEdit.getPatternFactory().getSuggestions(input, context) + .stream() + .map(s -> "^" + s); + } + + private @NotNull PartiallyApplyingComponents split(String input) { + String type; + String properties = ""; + //default as delete NBT retains previous behaviour + String nbt = compatibilityMode ? "{=}" : ""; + + int startProperties = input.indexOf('['); + int startNbt = input.indexOf('{'); + if (startNbt >= 0 && startNbt < startProperties) { + startProperties = -1; + } + + if (startProperties >= 0 && startNbt >= 0) { + //properties and nbt and maybe type + type = input.substring(0, startProperties); + properties = input.substring(startProperties, startNbt); + nbt = input.substring(startNbt); + } else if (startProperties >= 0) { + //properties and maybe type + type = input.substring(0, startProperties); + properties = input.substring(startProperties); + } else if (startNbt >= 0) { + //nbt and maybe type + type = input.substring(0, startNbt); + nbt = input.substring(startNbt); + } else { + type = input; + } + return new PartiallyApplyingComponents(type, properties, nbt); + } + + private record PartiallyApplyingComponents(String type, String properties, String nbt) { + } + + @Override + public Pattern parseFromInput(String input, ParserContext context) throws InputParseException { + if (!input.startsWith("^")) { + return null; + } + Extent extent = context.requireExtent(); + input = input.substring(1); + + if (input.isEmpty()) { + throw new NoMatchException(TranslatableComponent.of("worldedit.error.unknown-block", TextComponent.of(input))); + } + + PartiallyApplyingComponents components = split(input); + + List extendPatternFactories = new ArrayList<>(); + + if (!components.nbt().isEmpty()) { + extendPatternFactories + .add(getNbtApplyingPatternFactory(input, components.nbt())); + } + if (!components.type().isEmpty()) { + extendPatternFactories + .add(getTypeApplyingPatternFactory(context, components.type())); + } + if (!components.properties().isEmpty()) { + extendPatternFactories + .add(getStateApplyingPatternFactory(components)); + } + + if (extendPatternFactories.size() > 1) { + Extent buffer = new ExtentBuffer(extent); + Pattern[] patterns = extendPatternFactories.stream() + .map(factory -> factory.forExtend(buffer)) + .toArray(Pattern[]::new); + return new ExtentBufferedCompositePattern(buffer, patterns); + } + + return extendPatternFactories.getFirst().forExtend(extent); + + } + + private @NotNull ExtendPatternFactory getTypeApplyingPatternFactory(ParserContext context, String type) throws InputParseException { + Pattern pattern = worldEdit.getPatternFactory().parseFromInput(type, context); + return ext -> new TypeApplyingPattern(ext, pattern); + } + + private static @NotNull ExtendPatternFactory getStateApplyingPatternFactory(PartiallyApplyingComponents components) throws InputParseException { + String properties = components.properties(); + if (!properties.endsWith("]")) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbracket")); + } + String propertiesWithoutBrackets = properties.substring(1, properties.length() - 1); + final String[] states = propertiesWithoutBrackets.split(",", 0); + Map statesToSet = new HashMap<>(); + for (String state : states) { + if (state.isEmpty()) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-state")); + } + String[] propVal = state.split("=", 2); + if (propVal.length != 2) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-equals-separator")); + } + final String prop = propVal[0]; + if (prop.isEmpty()) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-property")); + } + final String value = propVal[1]; + if (value.isEmpty()) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-value")); + } + if (statesToSet.put(prop, value) != null) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.duplicate-property", TextComponent.of(prop))); + } + } + return ext -> new StateApplyingPattern(ext, statesToSet); + } + + private static @NotNull ExtendPatternFactory getNbtApplyingPatternFactory(String input, String nbt) throws InputParseException { + if (!nbt.endsWith("}")) { + throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbrace")); + } + if (nbt.equals("{}")) { + return (ext) -> new NBTApplyingPattern(ext, null); + } + boolean merge = true; + if (nbt.startsWith("{=")) { + merge = false; + nbt = "{" + nbt.substring(2); + } + LinCompoundTag tag; + try { + if (nbt.equals("{}")) { + tag = LinCompoundTag.builder().build(); + } else { + tag = LinStringIO.readFromStringUsing(nbt, LinCompoundTag::readFrom); + } + } catch (NbtParseException e) { + throw new NoMatchException(TranslatableComponent.of( + "worldedit.error.parser.invalid-nbt", + TextComponent.of("^" + input), + TextComponent.of(e.getMessage()) + )); + } + if (merge) { + return (ext) -> new NBTMergingPattern(ext, tag.value()); + } else { + return (ext) -> new NBTApplyingPattern(ext, tag); + } + } + + private interface ExtendPatternFactory { + Pattern forExtend(Extent e); + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/TypeOrStateApplyingPatternParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/TypeOrStateApplyingPatternParser.java index b7a6f187f8..681e969fe7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/TypeOrStateApplyingPatternParser.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/pattern/TypeOrStateApplyingPatternParser.java @@ -20,109 +20,10 @@ package com.sk89q.worldedit.extension.factory.parser.pattern; import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.command.util.SuggestionHelper; -import com.sk89q.worldedit.extension.input.InputParseException; -import com.sk89q.worldedit.extension.input.ParserContext; -import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.extent.buffer.ExtentBuffer; -import com.sk89q.worldedit.function.pattern.ExtentBufferedCompositePattern; -import com.sk89q.worldedit.function.pattern.Pattern; -import com.sk89q.worldedit.function.pattern.StateApplyingPattern; -import com.sk89q.worldedit.function.pattern.TypeApplyingPattern; -import com.sk89q.worldedit.internal.registry.InputParser; -import com.sk89q.worldedit.util.formatting.text.TextComponent; -import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; -import com.sk89q.worldedit.world.block.BlockType; -import com.sk89q.worldedit.world.block.BlockTypes; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Stream; - - -public class TypeOrStateApplyingPatternParser extends InputParser { +@Deprecated +public class TypeOrStateApplyingPatternParser extends PartiallyApplyingPatternParser{ public TypeOrStateApplyingPatternParser(WorldEdit worldEdit) { - super(worldEdit); - } - - @Override - public Stream getSuggestions(String input, ParserContext context) { - if (input.isEmpty()) { - return Stream.of("^"); - } - if (!input.startsWith("^")) { - return Stream.empty(); - } - input = input.substring(1); - - String[] parts = input.split("\\[", 2); - String type = parts[0]; - - if (parts.length == 1 || input.startsWith("#")) { - return worldEdit.getPatternFactory().getSuggestions(input, context).stream().map(s -> "^" + s); - } else { - if (type.isEmpty()) { - return Stream.empty(); // without knowing a type, we can't really suggest states - } else { - BlockType blockType = BlockTypes.get(type.toLowerCase(Locale.ROOT)); - return SuggestionHelper.getBlockPropertySuggestions(type, blockType, parts[1]).map(s -> "^" + s); - } - } - } - - @Override - public Pattern parseFromInput(String input, ParserContext context) throws InputParseException { - if (!input.startsWith("^")) { - return null; - } - Extent extent = context.requireExtent(); - input = input.substring(1); - - String[] parts = input.split("\\[", 2); - String type = parts[0]; - - if (parts.length == 1 || input.startsWith("#")) { - // This is something we can likely parse as a pattern directly - return new TypeApplyingPattern(extent, worldEdit.getPatternFactory().parseFromInput(input, context)); - } else { - // states given - if (!parts[1].endsWith("]")) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbracket")); - } - final String[] states = parts[1].substring(0, parts[1].length() - 1).split(",", 0); - Map statesToSet = new HashMap<>(); - for (String state : states) { - if (state.isEmpty()) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-state")); - } - String[] propVal = state.split("=", 2); - if (propVal.length != 2) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-equals-separator")); - } - final String prop = propVal[0]; - if (prop.isEmpty()) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-property")); - } - final String value = propVal[1]; - if (value.isEmpty()) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.empty-value")); - } - if (statesToSet.put(prop, value) != null) { - throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.duplicate-property", TextComponent.of(prop))); - } - } - if (type.isEmpty()) { - return new StateApplyingPattern(extent, statesToSet); - } else { - Extent buffer = new ExtentBuffer(extent); - Pattern typeApplier = new TypeApplyingPattern(buffer, - worldEdit.getBlockFactory().parseFromInput(type, context).getBlockType().getDefaultState()); - Pattern stateApplier = new StateApplyingPattern(buffer, statesToSet); - return new ExtentBufferedCompositePattern(buffer, typeApplier, stateApplier); - } - } + super(worldEdit, true); } - } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/ExtentBufferedCompositePattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/ExtentBufferedCompositePattern.java index 5583e55087..7f1eee4a7d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/ExtentBufferedCompositePattern.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/ExtentBufferedCompositePattern.java @@ -19,6 +19,7 @@ package com.sk89q.worldedit.function.pattern; +import com.google.common.annotations.VisibleForTesting; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.extent.buffer.ExtentBuffer; @@ -63,4 +64,9 @@ public BaseBlock applyBlock(BlockVector3 position) { } return lastBlock; } + + @VisibleForTesting + public Pattern[] getPatterns() { + return patterns; + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTApplyingPattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTApplyingPattern.java new file mode 100644 index 0000000000..7d69d26ff2 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTApplyingPattern.java @@ -0,0 +1,47 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.function.pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockState; +import org.enginehub.linbus.tree.LinCompoundTag; + +public class NBTApplyingPattern extends AbstractExtentPattern { + private final LinCompoundTag nbtToApply; + + public NBTApplyingPattern(Extent extent, LinCompoundTag nbtToApply) { + super(extent); + this.nbtToApply = nbtToApply; + } + + @Override + public BaseBlock applyBlock(BlockVector3 position) { + BlockState block = getExtent().getBlock(position); + return block.toBaseBlock(nbtToApply); + } + + @VisibleForTesting + public LinCompoundTag getNbtToApply() { + return nbtToApply; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTMergingPattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTMergingPattern.java new file mode 100644 index 0000000000..ad4826868b --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/NBTMergingPattern.java @@ -0,0 +1,56 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.function.pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BaseBlock; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinTag; + +import java.util.Map; + +public class NBTMergingPattern extends AbstractExtentPattern { + private final Map> nbtToMerge; + + public NBTMergingPattern(Extent extent, Map> nbtToMerge) { + super(extent); + this.nbtToMerge = nbtToMerge; + } + + @Override + public BaseBlock applyBlock(BlockVector3 position) { + BaseBlock baseBlock = getExtent().getFullBlock(position); + LinCompoundTag.Builder nbtBuilder; + if (baseBlock.getNbt() != null) { + nbtBuilder = baseBlock.getNbt().toBuilder(); + } else { + nbtBuilder = LinCompoundTag.builder(); + } + nbtBuilder.putAll(nbtToMerge); + return baseBlock.toBaseBlock(nbtBuilder.build()); + } + + @VisibleForTesting + public Map> getNbtToMerge() { + return nbtToMerge; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/StateApplyingPattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/StateApplyingPattern.java index aa20c89f15..72e4a19c20 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/StateApplyingPattern.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/StateApplyingPattern.java @@ -19,12 +19,12 @@ package com.sk89q.worldedit.function.pattern; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.registry.state.Property; import com.sk89q.worldedit.world.block.BaseBlock; -import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import java.util.Map; @@ -44,11 +44,17 @@ public StateApplyingPattern(Extent extent, Map statesToSet) { @Override public BaseBlock applyBlock(BlockVector3 position) { - BlockState block = getExtent().getBlock(position); + BaseBlock block = getExtent().getFullBlock(position); for (Entry, Object> entry : cache .computeIfAbsent(block.getBlockType(), b -> resolveProperties(states, b)).entrySet()) { block = block.with(entry.getKey(), entry.getValue()); } - return block.toBaseBlock(); + return block; } + + @VisibleForTesting + public Map getStates() { + return states; + } + } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/TypeApplyingPattern.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/TypeApplyingPattern.java index 63c9b57754..30dfb89b83 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/TypeApplyingPattern.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/pattern/TypeApplyingPattern.java @@ -19,6 +19,7 @@ package com.sk89q.worldedit.function.pattern; +import com.google.common.annotations.VisibleForTesting; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.registry.state.Property; @@ -62,13 +63,18 @@ public TypeApplyingPattern(Extent extent, Pattern pattern) { @Override public BaseBlock applyBlock(BlockVector3 position) { - BlockState oldBlock = getExtent().getBlock(position); + BaseBlock oldBlock = getExtent().getFullBlock(position); BlockState newBlock = pattern.applyBlock(position).toImmutableState(); for (Entry, Object> entry : oldBlock.getStates().entrySet()) { @SuppressWarnings("unchecked") Property prop = (Property) entry.getKey(); newBlock = newBlock.with(prop, entry.getValue()); } - return newBlock.toBaseBlock(); + return newBlock.toBaseBlock(oldBlock.getNbtReference()); + } + + @VisibleForTesting + public Pattern getTypeProvidingPattern() { + return pattern; } } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParserTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParserTest.java new file mode 100644 index 0000000000..af3ed486a0 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/extension/factory/parser/pattern/PartiallyApplyingPatternParserTest.java @@ -0,0 +1,172 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.sk89q.worldedit.extension.factory.parser.pattern; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.factory.PatternFactory; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.function.pattern.*; +import com.sk89q.worldedit.world.World; +import org.enginehub.linbus.format.snbt.LinStringIO; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinTag; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PartiallyApplyingPatternParserTest { + + WorldEdit worldEditMock; + World worldMock; + PatternFactory patternFactoryMock; + ParserContext parserContext; + + PartiallyApplyingPatternParser partiallyApplyingPatternParser; + + @BeforeEach + void setUp() throws InputParseException { + worldEditMock = mock(WorldEdit.class); + worldMock = mock(World.class); + patternFactoryMock = mock(PatternFactory.class); + partiallyApplyingPatternParser = new PartiallyApplyingPatternParser(worldEditMock); + parserContext = new ParserContext(); + parserContext.setWorld(worldMock); + when(worldEditMock.getPatternFactory()) + .thenReturn(patternFactoryMock); + when(patternFactoryMock.parseFromInput(anyString(), eq(parserContext))) + .thenAnswer((InvocationOnMock invocation) -> new Pattern() { + @Override + public String toString() { + return invocation.getArgument(0); + } + } + ); + } + + /** + * Test ensuring various commands are parsed correctly. + * this includes all possible permutations of pattern types and uses a nbt with nesting and an included list. + */ + @Test + void parseFromInput() throws InputParseException { + + Pattern pattern = partiallyApplyingPatternParser.parseFromInput("^minecraft:bamboo_wall_sign", parserContext); + verifyTypeApplyingPattern(pattern, "minecraft:bamboo_wall_sign"); + + pattern = partiallyApplyingPatternParser.parseFromInput("^[facing=east,foo=bar]", parserContext); + verifyStateApplyingPattern(pattern, Map.of("facing","east", "foo","bar")); + + pattern = partiallyApplyingPatternParser.parseFromInput("^{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + verifyNbtMergingPattern(pattern, "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + + pattern = partiallyApplyingPatternParser.parseFromInput("^{=Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + verifyNbtApplyingPattern(pattern, "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + + + pattern = partiallyApplyingPatternParser.parseFromInput("^barrel[facing=east]", parserContext); + List patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyTypeApplyingPattern(patterns.getFirst(),"barrel"); + verifyStateApplyingPattern(patterns.get(1), Map.of("facing","east")); + + pattern = partiallyApplyingPatternParser.parseFromInput("^barrel{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtMergingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyTypeApplyingPattern(patterns.get(1),"barrel"); + + pattern = partiallyApplyingPatternParser.parseFromInput("^barrel{=Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtApplyingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyTypeApplyingPattern(patterns.get(1),"barrel"); + + pattern = partiallyApplyingPatternParser.parseFromInput("^[facing=east,foo=bar]{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtMergingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyStateApplyingPattern(patterns.get(1), Map.of("facing","east", "foo","bar")); + + pattern = partiallyApplyingPatternParser.parseFromInput("^[facing=east,foo=bar]{=Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtApplyingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyStateApplyingPattern(patterns.get(1), Map.of("facing","east", "foo","bar")); + + + pattern = partiallyApplyingPatternParser.parseFromInput("^dirt[facing=east,foo=bar]{=Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtApplyingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyTypeApplyingPattern(patterns.get(1),"dirt"); + verifyStateApplyingPattern(patterns.get(2), Map.of("facing","east", "foo","bar")); + + + //patterns with interestiing/different nested values + pattern = partiallyApplyingPatternParser.parseFromInput("^dirt,stone,cobblestone[facing=east,foo=bar]{=Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtApplyingPattern(patterns.getFirst(), "{Items:[{count:64,Slot:0b,id:\"minecraft:dirt\"},{count:64,Slot:1b,id:\"minecraft:dirt\"}]}"); + verifyTypeApplyingPattern(patterns.get(1),"dirt,stone,cobblestone"); + verifyStateApplyingPattern(patterns.get(2), Map.of("facing","east", "foo","bar")); + + pattern = partiallyApplyingPatternParser.parseFromInput("^barrel{count:64,Slot:0b,id:\"minecraft:dirt\"}", parserContext); + patterns = verifyCombinedPatternWithSubpattern(pattern); + verifyNbtMergingPattern(patterns.getFirst(), "{count:64,Slot:0b,id:\"minecraft:dirt\"}"); + verifyTypeApplyingPattern(patterns.get(1),"barrel"); + + } + + private List verifyCombinedPatternWithSubpattern(Pattern pattern) { + assertInstanceOf(ExtentBufferedCompositePattern.class, pattern); + return List.of(((ExtentBufferedCompositePattern) pattern).getPatterns()); + } + + private void verifyStateApplyingPattern(Pattern pattern, Map stateToValidate) { + assertInstanceOf(StateApplyingPattern.class, pattern); + assertEquals(stateToValidate, ((StateApplyingPattern)pattern).getStates()); + } + + private void verifyTypeApplyingPattern(Pattern pattern, String typeToValidate) { + assertInstanceOf(TypeApplyingPattern.class, pattern); + assertEquals(typeToValidate,((TypeApplyingPattern)pattern).getTypeProvidingPattern().toString()); + } + + private void verifyNbtMergingPattern(Pattern pattern, String nbtToValidate) { + assertInstanceOf(NBTMergingPattern.class, pattern); + assertNbtMatches(nbtToValidate, ((NBTMergingPattern) pattern).getNbtToMerge()); + } + + private void verifyNbtApplyingPattern(Pattern pattern, String nbtToValidate) { + assertInstanceOf(NBTApplyingPattern.class, pattern); + assertNbtMatches(nbtToValidate, ((NBTApplyingPattern) pattern).getNbtToApply()); + } + + private void assertNbtMatches(String nbtToValidate, LinCompoundTag nbtToApply) { + LinCompoundTag readCompoundTag = LinStringIO.readFromStringUsing(nbtToValidate, LinCompoundTag::readFrom); + assertEquals(readCompoundTag, nbtToApply); + } + + private void assertNbtMatches(String nbtToValidate, Map> nbtToMerge) { + LinCompoundTag readCompoundTag = LinStringIO.readFromStringUsing(nbtToValidate, LinCompoundTag::readFrom); + assertEquals(readCompoundTag, LinCompoundTag.of(nbtToMerge)); + } +} \ No newline at end of file