Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsing support for block NBT #2173

Merged
merged 6 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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('}');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, we might cut off NBT early if there's } in an NBT string, and someone doesn't put one at the end, resulting in a weird error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will result in an invalid NBT error

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
Loading