Skip to content

Commit

Permalink
Add parsing support for block NBT (#2173)
Browse files Browse the repository at this point in the history
* Add parsing support for block NBT

* Fixed parsing of NBT blocks failing due to missing ID

* Fix a few minor issues

* Fix a few more bugs around parsing. Still not working for lists due to a lin-bus issue

* Remove two unused patterns

* Fixed one final bug found in testing
  • Loading branch information
me4502 authored Oct 8, 2023
1 parent a85e2ba commit 8915e30
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 22 deletions.
53 changes: 53 additions & 0 deletions worldedit-core/src/main/java/com/sk89q/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* String utilities.
Expand Down Expand Up @@ -305,6 +307,20 @@ public static <T extends Enum<?>> T lookup(Map<String, T> lookup, String name, b
return type;
}

private static int indexOfRegEx(String input, Pattern pattern) {
Matcher m = pattern.matcher(input);
return m.find() ? m.start() : -1;
}

private static int lastIndexOfRegEx(String input, Pattern pattern) {
// Reverse the input string and run a forwards search on the backwards string
StringBuilder builder = new StringBuilder(input).reverse();
int reverseIndex = indexOfRegEx(builder.toString(), pattern);

// If it wasn't found return -1, otherwise take length - index - 1.
return (reverseIndex == -1) ? -1 : (input.length() - reverseIndex - 1);
}

public static List<String> parseListInQuotes(String[] input, char delimiter, char quoteOpen, char quoteClose) {
return parseListInQuotes(input, delimiter, quoteOpen, quoteClose, false);
}
Expand All @@ -331,4 +347,41 @@ public static List<String> parseListInQuotes(String[] input, char delimiter, cha

return parsableBlocks;
}

public static List<String> parseListInQuotes(String[] input, char delimiter, char[] quoteOpen, char[] quoteClose, boolean appendLeftover) {
List<String> parsableBlocks = new ArrayList<>();
StringBuilder buffer = new StringBuilder();
int quotes = quoteOpen.length;
if (quotes != quoteClose.length) {
throw new Error("Mismatched quoteOpen and quoteClose lengths");
}
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);
}
}
}
if (appendLeftover && buffer.length() != 0) {
parsableBlocks.add(buffer.delete(buffer.length() - 1, buffer.length()).toString());
}

return parsableBlocks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public BlockFactory(WorldEdit worldEdit) {
public Set<BaseBlock> parseFromListInput(String input, ParserContext context) throws InputParseException {
Set<BaseBlock> blocks = new HashSet<>();
String[] splits = input.split(",");
for (String token : StringUtil.parseListInQuotes(splits, ',', '[', ']', true)) {
for (String token : StringUtil.parseListInQuotes(splits, ',', new char[] {'[', '{' }, new char[] {']', '}'}, true)) {
blocks.add(parseFromInput(token, context));
}
return blocks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.util.HandSide;
import com.sk89q.worldedit.util.concurrency.LazyReference;
import com.sk89q.worldedit.util.formatting.text.TextComponent;
import com.sk89q.worldedit.util.formatting.text.TranslatableComponent;
import com.sk89q.worldedit.world.World;
Expand All @@ -52,6 +53,9 @@
import com.sk89q.worldedit.world.entity.EntityType;
import com.sk89q.worldedit.world.entity.EntityTypes;
import com.sk89q.worldedit.world.registry.LegacyMapper;
import org.enginehub.linbus.format.snbt.LinStringIO;
import org.enginehub.linbus.stream.exception.NbtParseException;
import org.enginehub.linbus.tree.LinCompoundTag;

import java.util.HashMap;
import java.util.Locale;
Expand Down Expand Up @@ -254,9 +258,13 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
if (blockAndExtraData.length == 0) {
throw new NoMatchException(TranslatableComponent.of("worldedit.error.unknown-block", TextComponent.of(input)));
}
blockAndExtraData[0] = woolMapper(blockAndExtraData[0]);
if (context.isTryingLegacy()) {
// Perform a legacy wool colour mapping
blockAndExtraData[0] = woolMapper(blockAndExtraData[0]);
}

BlockState state = null;
LinCompoundTag blockNbtData = null;

// Legacy matcher
if (context.isTryingLegacy()) {
Expand All @@ -278,21 +286,41 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa

if (state == null) {
String typeString;
String stateString = null;

int stateStart = blockAndExtraData[0].indexOf('[');
if (stateStart == -1) {
int nbtStart = blockAndExtraData[0].indexOf('{');
int typeEnd = stateStart == -1 ? nbtStart : nbtStart == -1 ? stateStart : Math.min(nbtStart, stateStart);

if (typeEnd == -1) {
typeString = blockAndExtraData[0];
} else {
typeString = blockAndExtraData[0].substring(0, stateStart);
typeString = blockAndExtraData[0].substring(0, typeEnd);
}

String stateString = null;
if (stateStart != -1 && (nbtStart == -1 || stateStart < nbtStart)) {
if (stateStart + 1 >= blockAndExtraData[0].length()) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.hanging-lbracket", TextComponent.of(stateStart)));
}
int stateEnd = blockAndExtraData[0].lastIndexOf(']');
int stateEnd = blockAndExtraData[0].indexOf(']');
if (stateEnd < 0) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbracket"));
}
stateString = blockAndExtraData[0].substring(stateStart + 1, blockAndExtraData[0].length() - 1);
stateString = blockAndExtraData[0].substring(stateStart + 1, stateEnd);
}

String nbtString = null;
if (nbtStart != -1) {
if (nbtStart + 1 >= blockAndExtraData[0].length()) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.hanging-lbrace", TextComponent.of(nbtStart)));
}
int nbtEnd = blockAndExtraData[0].lastIndexOf('}');
if (nbtEnd < 0) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbrace"));
}
nbtString = blockAndExtraData[0].substring(nbtStart, nbtEnd + 1);
}

if (typeString.isEmpty()) {
throw new InputParseException(TranslatableComponent.of(
"worldedit.error.parser.bad-state-format",
Expand All @@ -313,6 +341,7 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa

blockType = blockInHand.getBlockType();
blockStates.putAll(blockInHand.getStates());
blockNbtData = blockInHand.getNbt();
} else if ("offhand".equalsIgnoreCase(typeString)) {
// Get the block type from the item in the user's off hand.
final BaseBlock blockInHand = getBlockInHand(context.requireActor(), HandSide.OFF_HAND);
Expand All @@ -322,6 +351,7 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa

blockType = blockInHand.getBlockType();
blockStates.putAll(blockInHand.getStates());
blockNbtData = blockInHand.getNbt();
} else if ("pos1".equalsIgnoreCase(typeString)) {
// Get the block type from the "primary position"
final World world = context.requireWorld();
Expand All @@ -331,10 +361,11 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
} catch (IncompleteRegionException e) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.incomplete-region"));
}
final BlockState blockInHand = world.getBlock(primaryPosition);
final BaseBlock blockInHand = world.getFullBlock(primaryPosition);

blockType = blockInHand.getBlockType();
blockStates.putAll(blockInHand.getStates());
blockNbtData = blockInHand.getNbt();
} else {
// Attempt to lookup a block from ID or name.
blockType = BlockTypes.get(typeString.toLowerCase(Locale.ROOT));
Expand Down Expand Up @@ -364,6 +395,24 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
state = state.with(objProp, blockState.getValue());
}
}

if (nbtString != null) {
LinCompoundTag otherTag;
try {
otherTag = LinStringIO.readFromStringUsing(nbtString, LinCompoundTag::readFrom);
} catch (NbtParseException e) {
throw new NoMatchException(TranslatableComponent.of(
"worldedit.error.parser.invalid-nbt",
TextComponent.of(input),
TextComponent.of(e.getMessage())
));
}
if (blockNbtData == null) {
blockNbtData = otherTag;
} else {
blockNbtData = blockNbtData.toBuilder().putAll(otherTag.value()).build();
}
}
}
// this should be impossible but IntelliJ isn't that smart
if (blockType == null) {
Expand All @@ -379,11 +428,13 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
}
}

BaseBlock baseBlock = state.toBaseBlock(blockNbtData == null ? null : LazyReference.computed(blockNbtData));

if (!context.isTryingLegacy()) {
return state.toBaseBlock();
return baseBlock;
}

if (DeprecationUtil.isSign(blockType)) {
if (DeprecationUtil.isSign(blockType) && blockAndExtraData.length > 1) {
// Allow special sign text syntax
String[] text = new String[4];
text[0] = blockAndExtraData.length > 1 ? blockAndExtraData[1] : "";
Expand All @@ -393,7 +444,7 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
@SuppressWarnings("deprecation")
SignBlock signBlock = new SignBlock(state, text);
return signBlock;
} else if (blockType == BlockTypes.SPAWNER) {
} else if (blockType == BlockTypes.SPAWNER && (blockAndExtraData.length > 1 || blockNbtData != null)) {
// Allow setting mob spawn type
String mobName;
if (blockAndExtraData.length > 1) {
Expand All @@ -412,7 +463,7 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
@SuppressWarnings("deprecation")
MobSpawnerBlock mobSpawnerBlock = new MobSpawnerBlock(state, mobName);
return mobSpawnerBlock;
} else if (blockType == BlockTypes.PLAYER_HEAD || blockType == BlockTypes.PLAYER_WALL_HEAD) {
} else if ((blockType == BlockTypes.PLAYER_HEAD || blockType == BlockTypes.PLAYER_WALL_HEAD) && (blockAndExtraData.length > 1 || blockNbtData != null)) {
// allow setting type/player/rotation
if (blockAndExtraData.length <= 1) {
@SuppressWarnings("deprecation")
Expand All @@ -426,7 +477,7 @@ private BaseBlock parseLogic(String input, ParserContext context) throws InputPa
SkullBlock skullBlock = new SkullBlock(state, type.replace(" ", "_")); // valid MC usernames
return skullBlock;
} else {
return state.toBaseBlock();
return baseBlock;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ public BaseItem parseFromInput(String input, ParserContext context) throws Input
} else {
typeString = input.substring(0, nbtStart);
if (nbtStart + 1 >= input.length()) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.hanging-lbracket", TextComponent.of(nbtStart)));
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.hanging-lbrace", TextComponent.of(nbtStart)));
}
int stateEnd = input.lastIndexOf('}');
if (stateEnd < 0) {
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbracket"));
throw new InputParseException(TranslatableComponent.of("worldedit.error.parser.missing-rbrace"));
}
nbtString = input.substring(nbtStart);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public RandomPatternParser(WorldEdit worldEdit) {
@Override
public Stream<String> getSuggestions(String input, ParserContext context) {
String[] splits = input.split(",", -1);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', new char[] {'[', '{' }, new char[] {']', '}'}, true);
// get suggestions for the last token only
String percent = null;
String token = patterns.get(patterns.size() - 1);
Expand All @@ -65,7 +65,7 @@ public Pattern parseFromInput(String input, ParserContext context) throws InputP
RandomPattern randomPattern = new RandomPattern();

String[] splits = input.split(",", -1);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', '[', ']', true);
List<String> patterns = StringUtil.parseListInQuotes(splits, ',', new char[] {'[', '{' }, new char[] {']', '}'}, true);
if (patterns.size() == 1) {
return null; // let a 'single'-pattern parser handle it
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ default <B extends BlockStateHolder<B>> boolean setBlock(BlockVector3 position,
if (block instanceof BaseBlock baseBlock) {
LinCompoundTag tag = baseBlock.getNbt();
if (tag != null) {
tag = tag.toBuilder()
.putString("id", baseBlock.getNbtId())
LinCompoundTag.Builder tagBuilder = tag.toBuilder()
.putInt("x", position.getX())
.putInt("y", position.getY())
.putInt("z", position.getZ())
.build();
.putInt("z", position.getZ());
if (!baseBlock.getNbtId().isBlank()) {
tagBuilder.putString("id", baseBlock.getNbtId());
}
tag = tagBuilder.build();

// update if TE changed as well
successful = updateTileEntity(pos, tag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.enginehub.linbus.format.snbt.LinStringIO;
import org.enginehub.linbus.stream.exception.NbtWriteException;
import org.enginehub.linbus.tree.LinCompoundTag;
import org.enginehub.linbus.tree.LinStringTag;
import org.enginehub.linbus.tree.LinTagType;

import java.util.Map;
Expand Down Expand Up @@ -121,7 +122,8 @@ public String getNbtId() {
if (nbtData == null) {
return "";
}
return nbtData.getValue().getTag("id", LinTagType.stringTag()).value();
LinStringTag idTag = nbtData.getValue().findTag("id", LinTagType.stringTag());
return idTag != null ? idTag.value() : "";
}

@Nullable
Expand Down
2 changes: 2 additions & 0 deletions worldedit-core/src/main/resources/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@
"worldedit.error.parser.negate-nothing": "Cannot negate nothing!",
"worldedit.error.parser.hanging-lbracket": "Invalid format. Hanging bracket at '{0}'.",
"worldedit.error.parser.missing-rbracket": "State is missing trailing ']'",
"worldedit.error.parser.hanging-lbrace": "Invalid format. Hanging brace at '{0}'.",
"worldedit.error.parser.missing-rbrace": "NBT is missing trailing '}'",
"worldedit.error.parser.missing-random-type": "Missing the type after the % symbol for '{0}'",
"worldedit.error.parser.clipboard.missing-coordinates": "Clipboard offset needs x,y,z coordinates.",
"worldedit.error.parser.player-only": "Input '{0}' requires a player!",
Expand Down

0 comments on commit 8915e30

Please sign in to comment.