diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/BinaryTagIO.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/BinaryTagIO.java new file mode 100644 index 000000000..7c7a4aba5 --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/BinaryTagIO.java @@ -0,0 +1,75 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +/** + * See https://github.com/KyoriPowered/adventure. + */ +public final class BinaryTagIO { + private BinaryTagIO() { + } + + /** + * Reads a compound tag from a {@link String}. + * + * @param input the string + * @return the compound tag + * @throws IOException if an exception was encountered while reading a compound tag + */ + @NotNull + public static CompoundTag readString(final @NotNull String input) throws IOException { + try { + final CharBuffer buffer = new CharBuffer(input); + final TagStringReader parser = new TagStringReader(buffer); + final CompoundTag tag = parser.compound(); + if (buffer.skipWhitespace().hasMore()) { + throw new IOException("Document had trailing content after first CompoundTag"); + } + return tag; + } catch (final StringTagParseException ex) { + throw new IOException(ex); + } + } + + /** + * Writes a compound tag to a {@link String}. + * + * @param tag the compound tag + * @return the string + * @throws IOException if an exception was encountered while writing the compound tag + */ + @NotNull + public static String writeString(final @NotNull CompoundTag tag) throws IOException { + final StringBuilder sb = new StringBuilder(); + try (final TagStringWriter emit = new TagStringWriter(sb)) { + emit.writeTag(tag); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/CharBuffer.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/CharBuffer.java new file mode 100644 index 000000000..0e12aaae1 --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/CharBuffer.java @@ -0,0 +1,120 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +/* package */ final class CharBuffer { + private final CharSequence sequence; + private int index; + + CharBuffer(final CharSequence sequence) { + this.sequence = sequence; + } + + /** + * Get the character at the current position + * + * @return The current character + */ + public char peek() { + return this.sequence.charAt(this.index); + } + + public char peek(final int offset) { + return this.sequence.charAt(this.index + offset); + } + + /** + * Get the current character and advance + * + * @return current character + */ + public char take() { + return this.sequence.charAt(this.index++); + } + + public boolean advance() { + this.index++; + return this.hasMore(); + } + + public boolean hasMore() { + return this.index < this.sequence.length(); + } + + /** + * Search for the provided token, and advance the reader index past the {@code until} character. + * + * @param until Case-insensitive token + * @return the string starting at the current position (inclusive) and going until the location of {@code until}, exclusive + */ + public CharSequence takeUntil(char until) throws StringTagParseException { + until = Character.toLowerCase(until); + int endIdx = -1; + for (int idx = this.index; idx < this.sequence.length(); ++idx) { + if (this.sequence.charAt(idx) == Tokens.ESCAPE_MARKER) { + idx++; + } else if (Character.toLowerCase(this.sequence.charAt(idx)) == until) { + endIdx = idx; + break; + } + } + if (endIdx == -1) { + throw this.makeError("No occurrence of " + until + " was found"); + } + + final CharSequence result = this.sequence.subSequence(this.index, endIdx); + this.index = endIdx + 1; + return result; + } + + /** + * Assert that the next non-whitespace character is the provided parameter. + * + *

If the assertion is successful, the token will be consumed.

+ * + * @param expectedChar expected character + * @return this + * @throws StringTagParseException if EOF or non-matching value is found + */ + public CharBuffer expect(final char expectedChar) throws StringTagParseException { + this.skipWhitespace(); + if (!this.hasMore()) { + throw this.makeError("Expected character '" + expectedChar + "' but got EOF"); + } + if (this.peek() != expectedChar) { + throw this.makeError("Expected character '" + expectedChar + "' but got '" + this.peek() + "'"); + } + this.take(); + return this; + } + + public CharBuffer skipWhitespace() { + while (this.hasMore() && Character.isWhitespace(this.peek())) this.advance(); + return this; + } + + public StringTagParseException makeError(final String message) { + return new StringTagParseException(message, this.sequence, this.index); + } +} diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/StringTagParseException.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/StringTagParseException.java new file mode 100644 index 000000000..626ed19ee --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/StringTagParseException.java @@ -0,0 +1,48 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +import java.io.IOException; + +/** + * An exception thrown when parsing a string tag + */ +/* package */ class StringTagParseException extends IOException { + private static final long serialVersionUID = -3001637554903912905L; + private final CharSequence buffer; + private final int position; + + public StringTagParseException(final String message, final CharSequence buffer, final int position) { + super(message); + this.buffer = buffer; + this.position = position; + } + + // TODO: Provide more specific position information + + @Override + public String getMessage() { + return super.getMessage() + "(at position " + this.position + ")"; + } +} diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringReader.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringReader.java new file mode 100644 index 000000000..f426b8cf0 --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringReader.java @@ -0,0 +1,315 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.FloatTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; +import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; + +/** + * See https://github.com/KyoriPowered/adventure. + */ +/* package */ final class TagStringReader { + private static final Field NAME_FIELD = getNameField(); + private final CharBuffer buffer; + + private static Field getNameField() { + try { + return Tag.class.getDeclaredField("name"); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + throw new IllegalArgumentException(e); + } + } + + public TagStringReader(final CharBuffer buffer) { + this.buffer = buffer; + } + + public CompoundTag compound() throws StringTagParseException { + this.buffer.expect(Tokens.COMPOUND_BEGIN); + final CompoundTag compoundTag = new CompoundTag(""); + while (this.buffer.hasMore()) { + final String key = this.key(); + final Tag tag = this.tag(); + // Doesn't get around this with the steveice lib :/ + try { + if (!NAME_FIELD.isAccessible()) { + NAME_FIELD.setAccessible(true); + } + NAME_FIELD.set(tag, key); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e); + } + + compoundTag.put(tag); + if (this.separatorOrCompleteWith(Tokens.COMPOUND_END)) { + return compoundTag; + } + } + throw this.buffer.makeError("Unterminated compound tag!"); + } + + public ListTag list() throws StringTagParseException { + final ListTag listTag = new ListTag(""); + this.buffer.expect(Tokens.ARRAY_BEGIN); + while (this.buffer.hasMore()) { + final Tag next = this.tag(); + // TODO: validate type + listTag.add(next); + if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { + return listTag; + } + } + throw this.buffer.makeError("Reached end of file without end of list tag!"); + } + + /** + * Similar to a list tag in syntax, but returning a single array tag rather than a list of tags. + * + * @return array-typed tag + */ + public Tag array(final char elementType) throws StringTagParseException { + this.buffer.expect(Tokens.ARRAY_BEGIN) + .expect(elementType) + .expect(Tokens.ARRAY_SIGNATURE_SEPARATOR); + + if (elementType == Tokens.TYPE_BYTE) { + return new ByteArrayTag("", this.byteArray()); + } else if (elementType == Tokens.TYPE_INT) { + return new IntArrayTag("", this.intArray()); + } else if (elementType == Tokens.TYPE_LONG) { + return new LongArrayTag("", this.longArray()); + } else { + throw this.buffer.makeError("Type " + elementType + " is not a valid element type in an array!"); + } + } + + private byte[] byteArray() throws StringTagParseException { + final List bytes = new ArrayList<>(); + while (this.buffer.hasMore()) { + final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_BYTE); + try { + bytes.add(Byte.valueOf(value.toString())); + } catch (final NumberFormatException ex) { + throw this.buffer.makeError("All elements of a byte array must be bytes!"); + } + + if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { + final byte[] result = new byte[bytes.size()]; + for (int i = 0; i < bytes.size(); ++i) { // todo yikes, let's do less boxing + result[i] = bytes.get(i); + } + return result; + } + } + throw this.buffer.makeError("Reached end of document without array close"); + } + + private int[] intArray() throws StringTagParseException { + final IntStream.Builder builder = IntStream.builder(); + while (this.buffer.hasMore()) { + final Tag value = this.tag(); + if (!(value instanceof IntTag)) { + throw this.buffer.makeError("All elements of an int array must be ints!"); + } + builder.add(((IntTag) value).getValue()); + if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { + return builder.build().toArray(); + } + } + throw this.buffer.makeError("Reached end of document without array close"); + } + + private long[] longArray() throws StringTagParseException { + final List longs = new ArrayList<>(); + while (this.buffer.hasMore()) { + final CharSequence value = this.buffer.skipWhitespace().takeUntil(Tokens.TYPE_LONG); + try { + longs.add(Long.valueOf(value.toString())); + } catch (final NumberFormatException ex) { + throw this.buffer.makeError("All elements of a long array must be longs!"); + } + + if (this.separatorOrCompleteWith(Tokens.ARRAY_END)) { + final long[] result = new long[longs.size()]; + for (int i = 0; i < longs.size(); ++i) { // todo yikes + result[i] = longs.get(i); + } + return result; + } + } + throw this.buffer.makeError("Reached end of document without array close"); + } + + public String key() throws StringTagParseException { + this.buffer.skipWhitespace(); + final char starChar = this.buffer.peek(); + try { + if (starChar == Tokens.SINGLE_QUOTE || starChar == Tokens.DOUBLE_QUOTE) { + return unescape(this.buffer.takeUntil(this.buffer.take()).toString()); + } + + final StringBuilder builder = new StringBuilder(); + while (Tokens.id(this.buffer.peek())) { + builder.append(this.buffer.take()); + } + return builder.toString(); + } finally { + this.buffer.expect(Tokens.COMPOUND_KEY_TERMINATOR); + } + } + + public Tag tag() throws StringTagParseException { + final char startToken = this.buffer.skipWhitespace().peek(); + switch (startToken) { + case Tokens.COMPOUND_BEGIN: + return this.compound(); + case Tokens.ARRAY_BEGIN: + if (this.buffer.peek(2) == ';') { // we know we're an array tag + return this.array(this.buffer.peek(1)); + } else { + return this.list(); + } + case Tokens.SINGLE_QUOTE: + case Tokens.DOUBLE_QUOTE: + // definitely a string tag + this.buffer.advance(); + return new StringTag("", unescape(this.buffer.takeUntil(startToken).toString())); + default: // scalar + return this.scalar(); + } + } + + /** + * A tag that is definitely some sort of scalar + * + *

Does not detect quoted strings, so

+ * + * @return a parsed tag + */ + private Tag scalar() { + final StringBuilder builder = new StringBuilder(); + boolean possiblyNumeric = true; + while (this.buffer.hasMore()) { + final char current = this.buffer.peek(); + if (possiblyNumeric && !Tokens.numeric(current)) { + if (builder.length() != 0) { + Tag result = null; + try { + switch (Character.toUpperCase(current)) { // try to read and return as a number + // case Tokens.TYPE_INTEGER: // handled below, ints are ~special~ + case Tokens.TYPE_BYTE: + result = new ByteTag("", Byte.parseByte(builder.toString())); + break; + case Tokens.TYPE_SHORT: + result = new ShortTag("", (Short.parseShort(builder.toString()))); + break; + case Tokens.TYPE_LONG: + result = new LongTag("", (Long.parseLong(builder.toString()))); + break; + case Tokens.TYPE_FLOAT: + result = new FloatTag("", (Float.parseFloat(builder.toString()))); + break; + case Tokens.TYPE_DOUBLE: + result = new DoubleTag("", (Double.parseDouble(builder.toString()))); + break; + } + } catch (final NumberFormatException ex) { + possiblyNumeric = false; // fallback to treating as a String + } + if (result != null) { + this.buffer.take(); + return result; + } + } + } + if (current == '\\') { // escape -- we are significantly more lenient than original format at the moment + this.buffer.advance(); + builder.append(this.buffer.take()); + } else if (Tokens.id(current)) { + builder.append(this.buffer.take()); + } else { // end of value + break; + } + } + // if we run out of content without an explicit value separator, then we're either an integer or string tag -- all others have a character at the end + final String built = builder.toString(); + if (possiblyNumeric) { + try { + return new IntTag("", Integer.parseInt(built)); + } catch (final NumberFormatException ex) { + // ignore + } + } + return new StringTag("", built); + + } + + private boolean separatorOrCompleteWith(final char endCharacter) throws StringTagParseException { + if (this.buffer.skipWhitespace().peek() == endCharacter) { + this.buffer.take(); + return true; + } + this.buffer.expect(Tokens.VALUE_SEPARATOR); + return false; + } + + + /** + * Remove simple escape sequences from a string + * + * @param withEscapes input string with escapes + * @return string with escapes processed + */ + private static String unescape(final String withEscapes) { + int escapeIdx = withEscapes.indexOf(Tokens.ESCAPE_MARKER); + if (escapeIdx == -1) { // nothing to unescape + return withEscapes; + } + int lastEscape = 0; + final StringBuilder output = new StringBuilder(withEscapes.length()); + do { + output.append(withEscapes, lastEscape, escapeIdx); + lastEscape = escapeIdx + 1; + } while ((escapeIdx = withEscapes.indexOf(Tokens.ESCAPE_MARKER, lastEscape + 1)) != -1); // add one extra character to make sure we don't include escaped backslashes + output.append(withEscapes.substring(lastEscape)); + return output.toString(); + } +} diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringWriter.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringWriter.java new file mode 100644 index 000000000..7534bdaa1 --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/TagStringWriter.java @@ -0,0 +1,256 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag; +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.FloatTag; +import com.github.steveice10.opennbt.tag.builtin.IntArrayTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.ListTag; +import com.github.steveice10.opennbt.tag.builtin.LongArrayTag; +import com.github.steveice10.opennbt.tag.builtin.LongTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; + +import java.io.IOException; +import java.io.Writer; + +/** + * See https://github.com/KyoriPowered/adventure. + */ +/* package */ final class TagStringWriter implements AutoCloseable { + private final Appendable out; + private final String indent = " "; // TODO: pretty-printing + private int level; + /** + * Whether a {@link Tokens#VALUE_SEPARATOR} needs to be printed before the beginning of the next object. + */ + private boolean needsSeparator; + + public TagStringWriter(final Appendable out) { + this.out = out; + } + + // NBT-specific + + public TagStringWriter writeTag(final Tag tag) throws IOException { + if (tag instanceof CompoundTag) { + return this.writeCompound((CompoundTag) tag); + } else if (tag instanceof ListTag) { + return this.writeList((ListTag) tag); + } else if (tag instanceof ByteArrayTag) { + return this.writeByteArray((ByteArrayTag) tag); + } else if (tag instanceof IntArrayTag) { + return this.writeIntArray((IntArrayTag) tag); + } else if (tag instanceof LongArrayTag) { + return this.writeLongArray((LongArrayTag) tag); + } else if (tag instanceof StringTag) { + return this.value(((StringTag) tag).getValue(), Tokens.EOF); + } else if (tag instanceof ByteTag) { + return this.value(Byte.toString(((ByteTag) tag).getValue()), Tokens.TYPE_BYTE); + } else if (tag instanceof ShortTag) { + return this.value(Short.toString(((ShortTag) tag).getValue()), Tokens.TYPE_SHORT); + } else if (tag instanceof IntTag) { + return this.value(Integer.toString(((IntTag) tag).getValue()), Tokens.TYPE_INT); + } else if (tag instanceof LongTag) { + return this.value(Long.toString(((LongTag) tag).getValue()), Tokens.TYPE_LONG); + } else if (tag instanceof FloatTag) { + return this.value(Float.toString(((FloatTag) tag).getValue()), Tokens.TYPE_FLOAT); + } else if (tag instanceof DoubleTag) { + return this.value(Double.toString(((DoubleTag) tag).getValue()), Tokens.TYPE_DOUBLE); + } else { + throw new IOException("Unknown tag type: " + tag.getClass().getSimpleName()); + // unknown! + } + } + + private TagStringWriter writeCompound(final CompoundTag tag) throws IOException { + this.beginCompound(); + for (Tag t : tag) { + this.key(t.getName()); + this.writeTag(t); + } + this.endCompound(); + return this; + } + + private TagStringWriter writeList(final ListTag tag) throws IOException { + this.beginList(); + for (final Tag el : tag) { + this.printAndResetSeparator(); + this.writeTag(el); + } + this.endList(); + return this; + } + + private TagStringWriter writeByteArray(final ByteArrayTag tag) throws IOException { + this.beginArray(Tokens.TYPE_BYTE); + + final byte[] value = tag.getValue(); + for (int i = 0, length = value.length; i < length; i++) { + this.printAndResetSeparator(); + this.value(Byte.toString(value[i]), Tokens.TYPE_BYTE); + } + this.endArray(); + return this; + } + + private TagStringWriter writeIntArray(final IntArrayTag tag) throws IOException { + this.beginArray(Tokens.TYPE_INT); + + final int[] value = tag.getValue(); + for (int i = 0, length = value.length; i < length; i++) { + this.printAndResetSeparator(); + this.value(Integer.toString(value[i]), Tokens.TYPE_INT); + } + this.endArray(); + return this; + } + + private TagStringWriter writeLongArray(final LongArrayTag tag) throws IOException { + this.beginArray(Tokens.TYPE_LONG); + + final long[] value = tag.getValue(); + for (int i = 0, length = value.length; i < length; i++) { + this.printAndResetSeparator(); + this.value(Long.toString(value[i]), Tokens.TYPE_LONG); + } + this.endArray(); + return this; + } + + // Value types + + public TagStringWriter beginCompound() throws IOException { + this.printAndResetSeparator(); + this.level++; + this.out.append(Tokens.COMPOUND_BEGIN); + return this; + } + + public TagStringWriter endCompound() throws IOException { + this.out.append(Tokens.COMPOUND_END); + this.level--; + this.needsSeparator = true; + return this; + } + + public TagStringWriter key(final String key) throws IOException { + this.printAndResetSeparator(); + this.writeMaybeQuoted(key, false); + this.out.append(Tokens.COMPOUND_KEY_TERMINATOR); // TODO: spacing/pretty-printing + return this; + } + + public TagStringWriter value(final String value, final char valueType) throws IOException { + if (valueType == Tokens.EOF) { // string doesn't have its type + this.writeMaybeQuoted(value, true); + } else { + this.out.append(value); + if (valueType != Tokens.TYPE_INT) { + this.out.append(valueType); + } + } + this.needsSeparator = true; + return this; + } + + public TagStringWriter beginList() throws IOException { + this.printAndResetSeparator(); + this.level++; + this.out.append(Tokens.ARRAY_BEGIN); + return this; + } + + public TagStringWriter endList() throws IOException { + this.out.append(Tokens.ARRAY_END); + this.level--; + this.needsSeparator = true; + return this; + } + + private TagStringWriter beginArray(final char type) throws IOException { + this.beginList() + .out.append(type) + .append(Tokens.ARRAY_SIGNATURE_SEPARATOR); + return this; + } + + private TagStringWriter endArray() throws IOException { + return this.endList(); + } + + private void writeMaybeQuoted(final String content, boolean requireQuotes) throws IOException { + if (!requireQuotes) { + for (int i = 0; i < content.length(); ++i) { + if (!Tokens.id(content.charAt(i))) { + requireQuotes = true; + break; + } + } + } + if (requireQuotes) { // TODO: single quotes + this.out.append(Tokens.DOUBLE_QUOTE); + this.out.append(escape(content, Tokens.DOUBLE_QUOTE)); + this.out.append(Tokens.DOUBLE_QUOTE); + } else { + this.out.append(content); + } + } + + private static String escape(final String content, final char quoteChar) { + final StringBuilder output = new StringBuilder(content.length()); + for (int i = 0; i < content.length(); ++i) { + final char c = content.charAt(i); + if (c == quoteChar || c == '\\') { + output.append(Tokens.ESCAPE_MARKER); + } + output.append(c); + } + return output.toString(); + } + + private void printAndResetSeparator() throws IOException { + if (this.needsSeparator) { + this.out.append(Tokens.VALUE_SEPARATOR); + this.needsSeparator = false; + } + } + + + @Override + public void close() throws IOException { + if (this.level != 0) { + throw new IllegalStateException("Document finished with unbalanced start and end objects"); + } + if (this.out instanceof Writer) { + ((Writer) this.out).flush(); + } + } +} diff --git a/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/Tokens.java b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/Tokens.java new file mode 100644 index 000000000..06e72393d --- /dev/null +++ b/common/src/main/java/us/myles/ViaVersion/api/minecraft/nbt/Tokens.java @@ -0,0 +1,88 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package us.myles.ViaVersion.api.minecraft.nbt; + +/** + * See https://github.com/KyoriPowered/adventure. + */ +/* package */ final class Tokens { + // Compounds + static final char COMPOUND_BEGIN = '{'; + static final char COMPOUND_END = '}'; + static final char COMPOUND_KEY_TERMINATOR = ':'; + + // Arrays + static final char ARRAY_BEGIN = '['; + static final char ARRAY_END = ']'; + static final char ARRAY_SIGNATURE_SEPARATOR = ';'; + + static final char VALUE_SEPARATOR = ','; + + static final char SINGLE_QUOTE = '\''; + static final char DOUBLE_QUOTE = '"'; + static final char ESCAPE_MARKER = '\\'; + + static final char TYPE_BYTE = 'B'; + static final char TYPE_SHORT = 'S'; + static final char TYPE_INT = 'I'; // array only + static final char TYPE_LONG = 'L'; + static final char TYPE_FLOAT = 'F'; + static final char TYPE_DOUBLE = 'D'; + + static final char EOF = '\0'; + + private Tokens() { + } + + /** + * Return if a character is a valid component in an identifier + * + *

An identifier character must match the expression {@code [a-zA-Z0-9_+.-]}

+ * + * @param c the character + * @return identifier + */ + static boolean id(final char c) { + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '-' || c == '_' + || c == '.' || c == '+'; + } + + /** + * Return whether a character could be at some position in a number. + *

+ * A string passing this check does not necessarily mean it is syntactically valid + * + * @param c character to check + * @return if possibly part of a number + */ + static boolean numeric(final char c) { + return (c >= '0' && c <= '9') // digit + || c == '+' || c == '-' // positive or negative + || c == 'e' || c == 'E' // exponent + || c == '.'; // decimal + } +} diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_13to1_12_2/ChatRewriter.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_13to1_12_2/ChatRewriter.java index 394b793b7..e3679254b 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_13to1_12_2/ChatRewriter.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_13to1_12_2/ChatRewriter.java @@ -1,16 +1,25 @@ package us.myles.ViaVersion.protocols.protocol1_13to1_12_2; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.ShortTag; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.chat.ComponentSerializer; +import us.myles.ViaVersion.api.Via; +import us.myles.ViaVersion.api.minecraft.item.Item; +import us.myles.ViaVersion.api.minecraft.nbt.BinaryTagIO; import us.myles.ViaVersion.api.rewriters.ComponentRewriter; import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.data.MappingData; +import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.packets.InventoryPackets; import us.myles.ViaVersion.util.GsonUtil; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -20,6 +29,71 @@ public class ChatRewriter { private static final Pattern URL = Pattern.compile("^(?:(https?)://)?([-\\w_.]{2,}\\.[a-z]{2,4})(/\\S*)?$"); private static final BaseComponent[] EMPTY_COMPONENTS = new BaseComponent[0]; private static final ComponentRewriter COMPONENT_REWRITER = new ComponentRewriter() { + @Override + protected void handleHoverEvent(JsonObject hoverEvent) { + super.handleHoverEvent(hoverEvent); + String action = hoverEvent.getAsJsonPrimitive("action").getAsString(); + if (!action.equals("show_item")) return; + + JsonElement value = hoverEvent.getAsJsonObject("value"); + if (value == null) return; + + String text = findItemNBT(value); + System.out.println(text); + try { + CompoundTag tag = BinaryTagIO.readString(text); + System.out.println(tag); + CompoundTag itemTag = tag.get("tag"); + ShortTag damageTag = tag.get("Damage"); + + // Call item converter + short damage = damageTag != null ? damageTag.getValue() : 0; + Item item = new Item(); + item.setData(damage); + item.setTag(itemTag); + InventoryPackets.toClient(item); + + // Serialize again + if (damage != item.getData()) { + tag.put(new ShortTag("Damage", item.getData())); + } + if (itemTag != null) { + tag.put(itemTag); + } + + JsonArray array = new JsonArray(); + JsonObject object = new JsonObject(); + array.add(object); + String serializedNBT = BinaryTagIO.writeString(tag); + object.addProperty("text", serializedNBT); + hoverEvent.add("value", array); + + System.out.println(serializedNBT); + } catch (IOException e) { + Via.getPlatform().getLogger().warning("Invalid NBT in show_item:"); + e.printStackTrace(); + } + } + + private String findItemNBT(JsonElement element) { + if (element.isJsonArray()) { + for (JsonElement jsonElement : element.getAsJsonArray()) { + String value = findItemNBT(jsonElement); + if (value != null) { + return value; + } + } + } else if (element.isJsonObject()) { + JsonPrimitive text = element.getAsJsonObject().getAsJsonPrimitive("text"); + if (text != null) { + return text.getAsString(); + } + } else if (element.isJsonPrimitive()) { + return element.getAsJsonPrimitive().getAsString(); + } + return null; + } + @Override protected void handleTranslate(JsonObject object, String translate) { super.handleTranslate(object, translate);