legacy tag parsing and transformation

This commit is contained in:
nea
2022-07-26 11:42:48 +02:00
parent 372eec27e5
commit 37218614c6
15 changed files with 373 additions and 164 deletions

View File

@@ -0,0 +1,232 @@
package moe.nea.notenoughupdates
import net.minecraft.nbt.*
import java.util.*
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<Int>()
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 = '0'..'9'
fun parse(string: String): CompoundTag {
return LegacyTagParser(string).baseTag
}
}
fun skipWhitespace() {
racer.consumeWhile { Character.isWhitespace(it.last()) } // Only check last since other chars are always checked before.
}
fun parseTag(): CompoundTag {
skipWhitespace()
racer.expect("{", "Expected '{ at start of tag")
skipWhitespace()
val tag = CompoundTag()
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(): Tag {
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(): ListTag {
skipWhitespace()
racer.expect("[", "Expected '[' at start of tag")
skipWhitespace()
val list = ListTag()
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(): StringTag {
return StringTag.valueOf(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(): NumericTag {
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 doubleMatch = Patterns.DOUBLE.matchEntire(textForm) ?: Patterns.DOUBLE_UNTYPED.matchEntire(textForm)
if (doubleMatch != null) {
return DoubleTag.valueOf(doubleMatch.groups[1]!!.value.toDouble())
}
val floatMatch = Patterns.FLOAT.matchEntire(textForm)
if (floatMatch != null) {
return FloatTag.valueOf(floatMatch.groups[1]!!.value.toFloat())
}
val byteMatch = Patterns.BYTE.matchEntire(textForm)
if (byteMatch != null) {
return ByteTag.valueOf(byteMatch.groups[1]!!.value.toByte())
}
val longMatch = Patterns.LONG.matchEntire(textForm)
if (longMatch != null) {
return LongTag.valueOf(longMatch.groups[1]!!.value.toLong())
}
val shortMatch = Patterns.SHORT.matchEntire(textForm)
if (shortMatch != null) {
return ShortTag.valueOf(shortMatch.groups[1]!!.value.toShort())
}
val integerMatch = Patterns.INTEGER.matchEntire(textForm)
if (integerMatch != null) {
return IntTag.valueOf(integerMatch.groups[1]!!.value.toInt())
}
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)
}
}
}

View File

@@ -0,0 +1,23 @@
package moe.nea.notenoughupdates
import dev.architectury.registry.registries.Registries
import io.github.moulberry.repo.NEURepository
import java.nio.file.Path
object NotEnoughUpdates {
val REGISTRIES by lazy { Registries.get(MOD_ID) }
const val MOD_ID = "notenoughupdates"
val neuRepo = NEURepository.of(Path.of("NotEnoughUpdates-REPO")).also {
it.reload()
}
fun init() {
}
}

View File

@@ -1,7 +1,8 @@
package moe.nea.notenoughupdates.rei
import com.mojang.blaze3d.vertex.PoseStack
import io.github.moulberry.repo.NEURepository
import com.mojang.serialization.Dynamic
import io.github.moulberry.repo.data.NEUItem
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer
@@ -16,45 +17,105 @@ import me.shedaniel.rei.api.common.entry.type.EntryType
import me.shedaniel.rei.api.common.entry.type.EntryTypeRegistry
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes
import me.shedaniel.rei.api.common.util.EntryStacks
import net.minecraft.core.Registry
import moe.nea.notenoughupdates.LegacyTagParser
import moe.nea.notenoughupdates.NotEnoughUpdates.neuRepo
import net.minecraft.ChatFormatting
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.StringTag
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.TextComponent
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.util.datafix.DataFixers.getDataFixer
import net.minecraft.util.datafix.fixes.References
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.enchantment.Enchantments
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Stream
class NEUReiPlugin : REIClientPlugin {
data class SBItem(val sbname: String, val backing: Item)
companion object {
fun EntryStack<NEUReiPlugin.SBItem>.asItemStack() =
EntryStack.of(VanillaEntryTypes.ITEM, ItemStack(this.value.backing).also {
it.enchant(Enchantments.BINDING_CURSE, 1)
it.hoverName = TextComponent(value.sbname)
})
fun EntryStack<NEUItem>.asItemEntry(): EntryStack<ItemStack> {
return EntryStack.of(VanillaEntryTypes.ITEM, value.asItemStack())
}
fun ItemStack.appendLore(args: List<Component>) {
val compoundTag = getOrCreateTagElement("display")
val loreList = compoundTag.getList("Lore", StringTag.TAG_STRING.toInt())
for (arg in args) {
loreList.add(StringTag.valueOf(Component.Serializer.toJson(arg)))
}
compoundTag.put("Lore", loreList)
}
val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
fun NEUItem.asItemStackNow(): ItemStack {
val df = getDataFixer()
val itemTag1_8_9 = CompoundTag()
itemTag1_8_9.put("tag", LegacyTagParser.parse(this.nbttag))
itemTag1_8_9.putString("id", this.minecraftItemId)
itemTag1_8_9.putByte("Count", 1)
itemTag1_8_9.putShort("Damage", this.damage.toShort())
val itemTag_modern = try {
df.update(
References.ITEM_STACK,
Dynamic(NbtOps.INSTANCE, itemTag1_8_9),
99,
2975
).value as CompoundTag
} catch (e: Exception) {
e.printStackTrace()
return ItemStack(Items.PAINTING).apply {
appendLore(listOf(TextComponent("Exception rendering item: $skyblockItemId")))
}
}
val itemInstance = ItemStack.of(itemTag_modern)
return itemInstance.also {
if(false)it.appendLore(
listOf(
TextComponent("Old: $minecraftItemId").withStyle {
it.withItalic(false).withColor(ChatFormatting.RED)
},
TextComponent("Modern: $itemTag_modern").withStyle {
it.withItalic(false).withColor(ChatFormatting.RED)
},
)
)
it.hoverName = TextComponent(this.skyblockItemId)
}
}
fun NEUItem.asItemStack(): ItemStack {
var s = cache[this.skyblockItemId]
if (s == null) {
s = asItemStackNow()
cache[this.skyblockItemId] = s
}
return s
}
val hehe = ResourceLocation("notenoughupdates", "skyblockitems")
}
object SBItemEntryDefinition : EntryDefinition<SBItem> {
override fun equals(o1: SBItem?, o2: SBItem?, context: ComparisonContext?): Boolean {
object SBItemEntryDefinition : EntryDefinition<NEUItem> {
override fun equals(o1: NEUItem?, o2: NEUItem?, context: ComparisonContext?): Boolean {
return o1 == o2
}
override fun getValueType(): Class<SBItem> = SBItem::class.java
override fun getType(): EntryType<SBItem> =
override fun getValueType(): Class<NEUItem> = NEUItem::class.java
override fun getType(): EntryType<NEUItem> =
EntryType.deferred(hehe)
override fun getRenderer(): EntryRenderer<SBItem> = object : EntryRenderer<SBItem> {
override fun getRenderer(): EntryRenderer<NEUItem> = object : EntryRenderer<NEUItem> {
override fun render(
entry: EntryStack<SBItem>,
entry: EntryStack<NEUItem>,
matrices: PoseStack,
bounds: Rectangle,
mouseX: Int,
@@ -63,75 +124,64 @@ class NEUReiPlugin : REIClientPlugin {
) {
VanillaEntryTypes.ITEM.definition.renderer
.render(
entry.asItemStack(),
entry.asItemEntry(),
matrices, bounds, mouseX, mouseY, delta
)
}
override fun getTooltip(entry: EntryStack<SBItem>, mouse: Point): Tooltip? {
override fun getTooltip(entry: EntryStack<NEUItem>, mouse: Point): Tooltip? {
return VanillaEntryTypes.ITEM.definition.renderer
.getTooltip(entry.asItemStack(), mouse)
.getTooltip(entry.asItemEntry(), mouse)
}
}
override fun getSerializer(): EntrySerializer<SBItem>? {
override fun getSerializer(): EntrySerializer<NEUItem>? {
return null
}
override fun getTagsFor(entry: EntryStack<SBItem>?, value: SBItem?): Stream<out TagKey<*>> {
override fun getTagsFor(entry: EntryStack<NEUItem>?, value: NEUItem?): Stream<out TagKey<*>> {
return Stream.empty()
}
override fun asFormattedText(entry: EntryStack<SBItem>, value: SBItem): Component {
return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemStack(), ItemStack(value.backing))
override fun asFormattedText(entry: EntryStack<NEUItem>, value: NEUItem): Component {
return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asItemStack())
}
override fun hash(entry: EntryStack<SBItem>, value: SBItem, context: ComparisonContext): Long {
return value.sbname.hashCode().toLong()
override fun hash(entry: EntryStack<NEUItem>, value: NEUItem, context: ComparisonContext): Long {
return value.skyblockItemId.hashCode().toLong()
}
override fun wildcard(entry: EntryStack<SBItem>, value: SBItem): SBItem {
override fun wildcard(entry: EntryStack<NEUItem>, value: NEUItem): NEUItem {
return value
}
override fun normalize(entry: EntryStack<SBItem>, value: SBItem): SBItem {
override fun normalize(entry: EntryStack<NEUItem>, value: NEUItem): NEUItem {
return value
}
override fun copy(entry: EntryStack<SBItem>?, value: SBItem): SBItem {
return value.copy()
override fun copy(entry: EntryStack<NEUItem>?, value: NEUItem): NEUItem {
return value
}
override fun isEmpty(entry: EntryStack<SBItem>?, value: SBItem?): Boolean {
override fun isEmpty(entry: EntryStack<NEUItem>?, value: NEUItem?): Boolean {
return false
}
override fun getIdentifier(entry: EntryStack<SBItem>?, value: SBItem): ResourceLocation? {
return ResourceLocation("skyblockitem", value.sbname)
override fun getIdentifier(entry: EntryStack<NEUItem>?, value: NEUItem): ResourceLocation {
return ResourceLocation("skyblockitem", value.skyblockItemId.lowercase().replace(";", "__"))
}
}
val neuRepo = NEURepository.of(Path.of("NotEnoughUpdates-REPO")).also {
it.reload()
}
override fun registerEntryTypes(registry: EntryTypeRegistry) {
registry.register(hehe, SBItemEntryDefinition)
}
override fun registerEntries(registry: EntryRegistry) {
neuRepo.items.items.values.forEach {
println("Adding item: $it")
registry.addEntry(
EntryStack.of(
SBItemEntryDefinition, SBItem(
it.skyblockItemId.lowercase().replace(";", "__"), Registry.ITEM.get(ResourceLocation(it.minecraftItemId))
)
)
)
registry.addEntry(EntryStack.of(SBItemEntryDefinition, it))
}
registry.addEntry(EntryStacks.of(ItemStack(Items.DIAMOND).also {
it.enchant(Enchantments.ALL_DAMAGE_PROTECTION, 10)

View File

@@ -1,30 +0,0 @@
package net.examplemod
import dev.architectury.injectables.annotations.ExpectPlatform
import dev.architectury.platform.Platform
import java.nio.file.Path
object ExampleExpectPlatform {
/**
* We can use [Platform.getConfigFolder] but this is just an example of [ExpectPlatform].
*
*
* This must be a **public static** method. The platform-implemented solution must be placed under a
* platform sub-package, with its class suffixed with `Impl`.
*
*
* Example:
* Expect: net.examplemod.ExampleExpectPlatform#getConfigDirectory()
* Actual Fabric: net.examplemod.fabric.ExampleExpectPlatformImpl#getConfigDirectory()
* Actual Forge: net.examplemod.forge.ExampleExpectPlatformImpl#getConfigDirectory()
*
*
* [You should also get the IntelliJ plugin to help with @ExpectPlatform.](https://plugins.jetbrains.com/plugin/16210-architectury)
*/
@ExpectPlatform
@JvmStatic
fun getConfigDirectory(): Path {
// Just throw an error, the content should get replaced at runtime.
throw AssertionError()
}
}

View File

@@ -1,31 +0,0 @@
package net.examplemod
import com.google.common.base.Suppliers
import dev.architectury.registry.CreativeTabRegistry
import dev.architectury.registry.registries.DeferredRegister
import dev.architectury.registry.registries.Registries
import dev.architectury.registry.registries.RegistrySupplier
import net.minecraft.core.Registry
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.CreativeModeTab
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import java.util.function.Supplier
object ExampleMod {
const val MOD_ID = "examplemod"
// We can use this if we don't want to use DeferredRegister
@Suppress("unused")
val REGISTRIES: Supplier<Registries> = Suppliers.memoize { Registries.get(MOD_ID) }
// Registering a new creative tab
val EXAMPLE_TAB: CreativeModeTab = CreativeTabRegistry.create(ResourceLocation(MOD_ID, "example_tab")) { ItemStack(EXAMPLE_ITEM.get()) }
val ITEMS: DeferredRegister<Item> = DeferredRegister.create(MOD_ID, Registry.ITEM_REGISTRY)
val EXAMPLE_ITEM: RegistrySupplier<Item> = ITEMS.register("example_item") { Item(Item.Properties().tab(EXAMPLE_TAB)) }
fun init() {
ITEMS.register()
println(ExampleExpectPlatform.getConfigDirectory().toAbsolutePath().normalize().toString())
}
}

View File

@@ -1,23 +0,0 @@
package net.examplemod.mixin
import net.minecraft.client.gui.screens.TitleScreen
import org.objectweb.asm.Opcodes
import org.spongepowered.asm.mixin.Mixin
import org.spongepowered.asm.mixin.injection.At
import org.spongepowered.asm.mixin.injection.Inject
import org.spongepowered.asm.mixin.injection.Redirect
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo
@Mixin(TitleScreen::class)
class MixinTitleScreen {
@Inject(at = [At("HEAD")], method = ["init()V"])
private fun init(info: CallbackInfo) {
println("Hello from example architectury common mixin!")
}
@Redirect(method = ["render"], at = At("FIELD", target = "minceraftEasterEgg", opcode = Opcodes.GETFIELD))
private fun nextFloat(t: TitleScreen): Boolean {
return true
}
}

View File

@@ -1,3 +0,0 @@
{
"item.examplemod.example_item": "Example Item"
}

View File

@@ -3,7 +3,6 @@
"package": "net.examplemod.mixin",
"compatibilityLevel": "JAVA_16",
"client": [
"MixinTitleScreen"
],
"mixins": [
],

View File

@@ -9,6 +9,11 @@ architectury {
loom {
accessWidenerPath.set(project(":common").loom.accessWidenerPath)
launches {
named("client") {
property("fabric.log.level", "info")
}
}
}
/**

View File

@@ -0,0 +1,10 @@
package moe.nea.notenoughupdates
import moe.nea.notenoughupdates.NotEnoughUpdates.init
import net.fabricmc.api.ModInitializer
class FabricMain : ModInitializer {
override fun onInitialize() {
init()
}
}

View File

@@ -1,13 +0,0 @@
package net.examplemod.fabric
import net.fabricmc.loader.api.FabricLoader
import java.nio.file.Path
import net.examplemod.ExampleExpectPlatform
object ExampleExpectPlatformImpl {
/**
* This is our actual method to [ExampleExpectPlatform.getConfigDirectory].
*/
@JvmStatic
fun getConfigDirectory(): Path = FabricLoader.getInstance().configDir
}

View File

@@ -1,10 +0,0 @@
package net.examplemod.fabric
import net.examplemod.ExampleMod.init
import net.fabricmc.api.ModInitializer
class ExampleModFabric : ModInitializer {
override fun onInitialize() {
init()
}
}

View File

@@ -16,7 +16,7 @@
"environment": "client",
"entrypoints": {
"main": [
"net.examplemod.fabric.ExampleModFabric"
"moe.nea.notenoughupdates.FabricMain"
],
"rei": [
"moe.nea.notenoughupdates.rei.NEUReiPlugin"

View File

@@ -1,16 +1,16 @@
package net.examplemod.forge
import dev.architectury.platform.forge.EventBuses
import net.examplemod.ExampleMod
import net.examplemod.ExampleMod.init
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.NotEnoughUpdates.init
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext
@Mod(ExampleMod.MOD_ID)
@Mod(NotEnoughUpdates.MOD_ID)
class ExampleModForge {
init {
// Submit our event bus to let architectury register our content on the right time
EventBuses.registerModEventBus(ExampleMod.MOD_ID, FMLJavaModLoadingContext.get().modEventBus)
EventBuses.registerModEventBus(NotEnoughUpdates.MOD_ID, FMLJavaModLoadingContext.get().modEventBus)
init()
}
}

View File

@@ -1,6 +1,6 @@
package net.examplemod.quilt
import net.examplemod.ExampleMod.init
import moe.nea.notenoughupdates.NotEnoughUpdates.init
import org.quiltmc.loader.api.ModContainer
import org.quiltmc.qsl.base.api.entrypoint.ModInitializer