package moe.nea.firmament.util import java.util.* import net.minecraft.nbt.AbstractNbtNumber import net.minecraft.nbt.NbtByte import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtDouble import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtFloat import net.minecraft.nbt.NbtInt import net.minecraft.nbt.NbtList import net.minecraft.nbt.NbtLong import net.minecraft.nbt.NbtShort import net.minecraft.nbt.NbtString class LegacyTagParser private constructor(string: String) { data class TagParsingException(val baseString: String, val offset: Int, val mes0: String) : Exception("$mes0 at $offset in `$baseString`.") class StringRacer(val backing: String) { var idx = 0 val stack = Stack() fun pushState() { stack.push(idx) } fun popState() { idx = stack.pop() } fun discardState() { stack.pop() } fun peek(count: Int): String { return backing.substring(minOf(idx, backing.length), minOf(idx + count, backing.length)) } fun finished(): Boolean { return peek(1).isEmpty() } fun peekReq(count: Int): String? { val p = peek(count) if (p.length != count) return null return p } fun consumeCountReq(count: Int): String? { val p = peekReq(count) if (p != null) idx += count return p } fun tryConsume(string: String): Boolean { val p = peek(string.length) if (p != string) return false idx += p.length return true } fun consumeWhile(shouldConsumeThisString: (String) -> Boolean): String { var lastString: String = "" while (true) { val nextString = lastString + peek(1) if (!shouldConsumeThisString(nextString)) { return lastString } idx++ lastString = nextString } } fun expect(search: String, errorMessage: String) { if (!tryConsume(search)) error(errorMessage) } fun error(errorMessage: String): Nothing { throw TagParsingException(backing, idx, errorMessage) } } val racer = StringRacer(string) val baseTag = parseTag() companion object { val digitRange = "0123456789-" fun parse(string: String): NbtCompound { return LegacyTagParser(string).baseTag } } fun skipWhitespace() { racer.consumeWhile { Character.isWhitespace(it.last()) } // Only check last since other chars are always checked before. } fun parseTag(): NbtCompound { skipWhitespace() racer.expect("{", "Expected '{’ at start of tag") skipWhitespace() val tag = NbtCompound() while (!racer.tryConsume("}")) { skipWhitespace() val lhs = parseIdentifier() skipWhitespace() racer.expect(":", "Expected ':' after identifier in tag") skipWhitespace() val rhs = parseAny() tag.put(lhs, rhs) racer.tryConsume(",") skipWhitespace() } return tag } private fun parseAny(): NbtElement { skipWhitespace() val nextChar = racer.peekReq(1) ?: racer.error("Expected new object, found EOF") return when { nextChar == "{" -> parseTag() nextChar == "[" -> parseList() nextChar == "\"" -> parseStringTag() nextChar.first() in (digitRange) -> parseNumericTag() else -> racer.error("Unexpected token found. Expected start of new element") } } fun parseList(): NbtList { skipWhitespace() racer.expect("[", "Expected '[' at start of tag") skipWhitespace() val list = NbtList() while (!racer.tryConsume("]")) { skipWhitespace() racer.pushState() val lhs = racer.consumeWhile { it.all { it in digitRange } } skipWhitespace() if (!racer.tryConsume(":") || lhs.isEmpty()) { // No prefixed 0: racer.popState() list.add(parseAny()) // Reparse our number (or not a number) as actual tag } else { racer.discardState() skipWhitespace() list.add(parseAny()) // Ignore prefix indexes. They should not be generated out of order by any vanilla implementation (which is what NEU should export). Instead append where it appears in order. } skipWhitespace() racer.tryConsume(",") } return list } fun parseQuotedString(): String { skipWhitespace() racer.expect("\"", "Expected '\"' at string start") val sb = StringBuilder() while (true) { when (val peek = racer.consumeCountReq(1)) { "\"" -> break "\\" -> { val escaped = racer.consumeCountReq(1) ?: racer.error("Unfinished backslash escape") if (escaped != "\"" && escaped != "\\") { // Surprisingly i couldn't find unicode escapes to be generated by the original minecraft 1.8.9 implementation racer.idx-- racer.error("Invalid backslash escape '$escaped'") } sb.append(escaped) } null -> racer.error("Unfinished string") else -> { sb.append(peek) } } } return sb.toString() } fun parseStringTag(): NbtString { return NbtString.of(parseQuotedString()) } object Patterns { val DOUBLE = "([-+]?[0-9]*\\.?[0-9]+)[d|D]".toRegex() val FLOAT = "([-+]?[0-9]*\\.?[0-9]+)[f|F]".toRegex() val BYTE = "([-+]?[0-9]+)[b|B]".toRegex() val LONG = "([-+]?[0-9]+)[l|L]".toRegex() val SHORT = "([-+]?[0-9]+)[s|S]".toRegex() val INTEGER = "([-+]?[0-9]+)".toRegex() val DOUBLE_UNTYPED = "([-+]?[0-9]*\\.?[0-9]+)".toRegex() val ROUGH_PATTERN = "[-+]?[0-9]*\\.?[0-9]*[dDbBfFlLsS]?".toRegex() } fun parseNumericTag(): AbstractNbtNumber { skipWhitespace() val textForm = racer.consumeWhile { Patterns.ROUGH_PATTERN.matchEntire(it) != null } if (textForm.isEmpty()) { racer.error("Expected numeric tag (starting with either -, +, . or a digit") } val floatMatch = Patterns.FLOAT.matchEntire(textForm) if (floatMatch != null) { return NbtFloat.of(floatMatch.groups[1]!!.value.toFloat()) } val byteMatch = Patterns.BYTE.matchEntire(textForm) if (byteMatch != null) { return NbtByte.of(byteMatch.groups[1]!!.value.toByte()) } val longMatch = Patterns.LONG.matchEntire(textForm) if (longMatch != null) { return NbtLong.of(longMatch.groups[1]!!.value.toLong()) } val shortMatch = Patterns.SHORT.matchEntire(textForm) if (shortMatch != null) { return NbtShort.of(shortMatch.groups[1]!!.value.toShort()) } val integerMatch = Patterns.INTEGER.matchEntire(textForm) if (integerMatch != null) { return NbtInt.of(integerMatch.groups[1]!!.value.toInt()) } val doubleMatch = Patterns.DOUBLE.matchEntire(textForm) ?: Patterns.DOUBLE_UNTYPED.matchEntire(textForm) if (doubleMatch != null) { return NbtDouble.of(doubleMatch.groups[1]!!.value.toDouble()) } throw IllegalStateException("Could not properly parse numeric tag '$textForm', despite passing rough verification. This is a bug in the LegacyTagParser") } private fun parseIdentifier(): String { skipWhitespace() if (racer.peek(1) == "\"") { return parseQuotedString() } return racer.consumeWhile { val x = it.last() x != ':' && !Character.isWhitespace(x) } } }