diff options
Diffstat (limited to 'src/main/kotlin/features/debug')
| -rw-r--r-- | src/main/kotlin/features/debug/AnimatedClothingScanner.kt | 193 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/DebugLogger.kt | 11 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/DebugView.kt | 29 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/DeveloperFeatures.kt | 78 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/ExportedTestConstantMeta.kt | 27 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/MinorTrolling.kt | 10 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/PowerUserTools.kt | 191 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/SkinPreviews.kt | 91 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/SoundVisualizer.kt | 65 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt | 256 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/itemeditor/ItemExporter.kt | 250 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt | 87 | ||||
| -rw-r--r-- | src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt | 318 |
13 files changed, 1481 insertions, 125 deletions
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..dc77115 --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.commands.arguments.ResourceKeyArgument +import net.minecraft.core.component.DataComponentType +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.Tag +import net.minecraft.nbt.NbtOps +import net.minecraft.core.registries.Registries +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.NbtPrism +import moe.nea.firmament.util.tr + +object AnimatedClothingScanner { + + data class LensOfFashionTheft<T>( + val prism: NbtPrism, + val component: DataComponentType<T>, + ) { + fun observe(itemStack: ItemStack): Collection<Tag> { + val x = itemStack.get(component) ?: return listOf() + val nbt = component.codecOrThrow().encodeStart(NbtOps.INSTANCE, x).orThrow + return prism.access(nbt) + } + } + + var lens: LensOfFashionTheft<*>? = null + var subject: Entity? = null + var history: MutableList<String> = mutableListOf() + val metaHistory: MutableList<List<String>> = mutableListOf() + + @OptIn(ExperimentalStdlibApi::class) + @Subscribe + fun onUpdate(event: EntityUpdateEvent) { + val s = subject ?: return + if (event.entity != s) return + val l = lens ?: return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + val formatted = (l.observe(it.second)).joinToString() + history.add(formatted) + // TODO: add a slot filter + } + } + } + + fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> { + return metaHistory.fold(history, reducer).shortenCycle() + } + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("stealthisfit") { + thenLiteral("clear") { + thenExecute { + subject = null + metaHistory.clear() + history.clear() + MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history")) + } + } + thenLiteral("copy") { + thenExecute { + val history = reduceHistory { a, b -> a + b } + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history")) + } + thenLiteral("deduplicated") { + thenExecute { + val history = reduceHistory { a, b -> + (a.toMutableSet() + b).toList() + } + copyHistory(history) + MC.sendChat( + tr( + "firmament.fitstealer.copied.deduplicated", + "Copied the deduplicated history" + ) + ) + } + } + thenLiteral("merged") { + thenExecute { + val history = reduceHistory(GChainReconciliation::reconcileCycles) + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history")) + } + } + } + thenLiteral("target") { + thenLiteral("self") { + thenExecute { + toggleObserve(MC.player!!) + } + } + thenLiteral("pet") { + thenExecute { + source.sendFeedback( + tr( + "firmament.fitstealer.stealingpet", + "Observing nearest marker armourstand" + ) + ) + val p = MC.player!! + val nearestPet = p.level.getEntitiesOfClass( + ArmorStand::class.java, + p.boundingBox.inflate(10.0), + { it.isMarker }) + .minBy { it.distanceToSqr(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.crosshairPickEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + ResourceKeyArgument.key(Registries.DATA_COMPONENT_TYPE) + ) { component -> + thenArgument("path", NbtPrism.Argument) { path -> + thenExecute { + lens = LensOfFashionTheft( + get(path), + MC.unsafeGetRegistryEntry(get(component))!!, + ) + source.sendFeedback( + tr( + "firmament.fitstealer.lensset", + "Analyzing path ${get(path)} for component ${get(component).location()}" + ) + ) + } + } + } + } + } + } + } + + private fun copyHistory(toCopy: List<String>) { + ClipboardUtils.setTextContent(toCopy.joinToString("\n")) + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + subject = null + if (history.isNotEmpty()) { + metaHistory.add(history) + history = mutableListOf() + } + } + + private fun toggleObserve(entity: Entity?) { + subject = if (subject == null) entity else null + if (subject == null) { + metaHistory.add(history) + history = mutableListOf() + } + MC.sendChat( + subject?.let { + tr( + "firmament.fitstealer.targeted", + "Observing the equipment of ${it.name}." + ) + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } +} diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt index 2c6b962..2c8ced0 100644 --- a/src/main/kotlin/features/debug/DebugLogger.kt +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -1,24 +1,29 @@ package moe.nea.firmament.features.debug import kotlinx.serialization.serializer -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TestUtil import moe.nea.firmament.util.collections.InstanceList +import moe.nea.firmament.util.data.Config import moe.nea.firmament.util.data.DataHolder class DebugLogger(val tag: String) { companion object { val allInstances = InstanceList<DebugLogger>("DebugLogger") } + + @Config object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf) init { allInstances.add(this) } - fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun isEnabled() = TestUtil.isInTest || EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } fun log(text: () -> String) { if (!isEnabled()) return - MC.sendChat(Text.literal(text())) + MC.sendChat(Component.literal(text())) } } diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt deleted file mode 100644 index ee54260..0000000 --- a/src/main/kotlin/features/debug/DebugView.kt +++ /dev/null @@ -1,29 +0,0 @@ - - -package moe.nea.firmament.features.debug - -import moe.nea.firmament.Firmament -import moe.nea.firmament.annotations.Subscribe -import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.util.TimeMark - -object DebugView : FirmamentFeature { - private data class StoredVariable<T>( - val obj: T, - val timer: TimeMark, - ) - - private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf() - override val identifier: String - get() = "debug-view" - override val defaultEnabled: Boolean - get() = Firmament.DEBUG - - fun <T : Any?> showVariable(label: String, obj: T) { - synchronized(this) { - storedVariables[label] = StoredVariable(obj, TimeMark.now()) - } - } - -} diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt index 8f0c25c..8638bb6 100644 --- a/src/main/kotlin/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -3,33 +3,38 @@ package moe.nea.firmament.features.debug import java.io.File import java.nio.file.Path import java.util.concurrent.CompletableFuture +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.Mixin import kotlinx.serialization.json.encodeToStream import kotlin.io.path.absolute import kotlin.io.path.exists -import net.minecraft.client.MinecraftClient -import net.minecraft.text.Text +import net.minecraft.client.Minecraft +import net.minecraft.network.chat.Component import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.DebugInstantiateEvent import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.init.MixinPlugin import moe.nea.firmament.util.MC import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.asm.AsmAnnotationUtil +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.iterate -object DeveloperFeatures : FirmamentFeature { - override val identifier: String +object DeveloperFeatures { + val DEVELOPER_SUBCOMMAND: String = "dev" + val identifier: String get() = "developer" - override val config: TConfig - get() = TConfig - override val defaultEnabled: Boolean - get() = Firmament.DEBUG val gradleDir = Path.of(".").absolute() .iterate { it.parent } .find { it.resolve("settings.gradle.kts").exists() } + @Config object TConfig : ManagedConfig("developer", Category.DEV) { val autoRebuildResources by toggle("auto-rebuild") { false } } @@ -42,6 +47,42 @@ object DeveloperFeatures : FirmamentFeature { } @Subscribe + fun loadAllMixinClasses(event: DebugInstantiateEvent) { + val allMixinClasses = mutableSetOf<String>() + MixinPlugin.instances.forEach { plugin -> + val prefix = plugin.mixinPackage + "." + val classes = plugin.mixins.map { prefix + it } + allMixinClasses.addAll(classes) + for (cls in classes) { + val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use { + val node = ClassNode() + ClassReader(it).accept(node, 0) + val mixins = mutableListOf<Mixin>() + (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach { + val annotationType = Type.getType(it.desc) + val mixinType = Type.getType(Mixin::class.java) + if (mixinType == annotationType) { + mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it)) + } + } + mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } } + } + for (target in targets) + try { + Firmament.logger.debug("Loading ${target} to force instantiate ${cls}") + Class.forName(target, true, javaClass.classLoader) + } catch (ex: Throwable) { + Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex) + } + } + } + Firmament.logger.info("Forceloaded all Firmament mixins:") + val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet() + applied.forEach { Firmament.logger.info(" - ${it}") } + require(allMixinClasses == applied) + } + + @Subscribe fun dumpMissingTranslations(tickEvent: TickEvent) { val toDump = missingTranslations ?: return missingTranslations = null @@ -51,24 +92,27 @@ object DeveloperFeatures : FirmamentFeature { } @JvmStatic - fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> { - val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) { + fun hookOnBeforeResourceReload(client: Minecraft): CompletableFuture<Void> { + val reloadFuture = if (TConfig.autoRebuildResources && Firmament.DEBUG && gradleDir != null) { val builder = ProcessBuilder("./gradlew", ":processResources") builder.directory(gradleDir.toFile()) builder.inheritIO() val process = builder.start() - MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start")) + MC.sendChat(Component.translatable("firmament.dev.resourcerebuild.start")) val startTime = TimeMark.now() process.toHandle().onExit().thenApply { - MC.sendChat(Text.stringifiedTranslatable( - "firmament.dev.resourcerebuild.done", - startTime.passedTime())) + MC.sendChat( + Component.translatableEscape( + "firmament.dev.resourcerebuild.done", + startTime.passedTime() + ) + ) Unit } } else { CompletableFuture.completedFuture(Unit) } - return reloadFuture.thenCompose { client.reloadResources() } + return reloadFuture.thenCompose { client.reloadResourcePacks() } } } diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..a2b42fd --- /dev/null +++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional +import net.minecraft.SharedConstants +import moe.nea.firmament.Firmament + +data class ExportedTestConstantMeta( + val dataVersion: Int, + val modVersion: Optional<String>, +) { + companion object { + val current = ExportedTestConstantMeta( + SharedConstants.getCurrentVersion().dataVersion().version, + Optional.of("Firmament ${Firmament.version.friendlyString}") + ) + + val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create { + it.group( + Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion), + Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion), + ).apply(it, ::ExportedTestConstantMeta) + } + val SOURCE_CODEC = CODEC.fieldOf("source").codec() + } +} diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt index 32035a6..7936521 100644 --- a/src/main/kotlin/features/debug/MinorTrolling.kt +++ b/src/main/kotlin/features/debug/MinorTrolling.kt @@ -2,15 +2,13 @@ package moe.nea.firmament.features.debug -import net.minecraft.text.Text +import net.minecraft.network.chat.Component import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.ModifyChatEvent -import moe.nea.firmament.features.FirmamentFeature - // In memorian Dulkir -object MinorTrolling : FirmamentFeature { - override val identifier: String +object MinorTrolling { + val identifier: String get() = "minor-trolling" val trollers = listOf("nea89o", "lrg89") @@ -22,6 +20,6 @@ object MinorTrolling : FirmamentFeature { val (_, name, text) = m.groupValues if (name !in trollers) return if (!text.startsWith("c:")) return - it.replaceWith = Text.literal(text.substring(2).replace("&", "§")) + it.replaceWith = Component.literal(text.substring(2).replace("&", "§")) } } diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 225bc13..145ea35 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -2,43 +2,55 @@ package moe.nea.firmament.features.debug import com.mojang.serialization.JsonOps import kotlin.jvm.optionals.getOrNull -import net.minecraft.block.SkullBlock -import net.minecraft.block.entity.SkullBlockEntity -import net.minecraft.component.DataComponentTypes -import net.minecraft.component.type.ProfileComponent -import net.minecraft.entity.Entity -import net.minecraft.entity.LivingEntity -import net.minecraft.item.ItemStack -import net.minecraft.item.Items +import net.minecraft.world.level.block.SkullBlock +import net.minecraft.world.level.block.entity.SkullBlockEntity +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ListTag import net.minecraft.nbt.NbtOps -import net.minecraft.text.Text -import net.minecraft.text.TextCodecs -import net.minecraft.util.Identifier -import net.minecraft.util.hit.BlockHitResult -import net.minecraft.util.hit.EntityHitResult -import net.minecraft.util.hit.HitResult +import net.minecraft.advancements.critereon.NbtPredicate +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.Nameable +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.EntityHitResult +import net.minecraft.world.phys.HitResult import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotRenderEvents import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.WorldKeyboardEvent -import moe.nea.firmament.features.FirmamentFeature -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.mc.IntrospectableItemModelManager +import moe.nea.firmament.util.mc.SNbtFormatter import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.iterableArmorItems import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.unsafeNbt import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr -object PowerUserTools : FirmamentFeature { - override val identifier: String +object PowerUserTools { + val identifier: String get() = "power-user" + @Config object TConfig : ManagedConfig(identifier, Category.DEV) { val showItemIds by toggle("show-item-id") { false } val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") @@ -48,22 +60,26 @@ object PowerUserTools : FirmamentFeature { val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") val copyEntityData by keyBindingWithDefaultUnbound("entity-data") val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack") + val copyTitle by keyBindingWithDefaultUnbound("copy-title") + val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack") + val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe") + val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location") + val highlightNonOverlayItems by toggle("highlight-non-overlay") { false } + val dontHighlightSemicolonItems by toggle("dont-highlight-semicolon-items") { false } + val showSlotNumbers by keyBindingWithDefaultUnbound("slot-numbers") + val autoCopyAnimatedSkins by toggle("copy-animated-skins") { false } } - override val config - get() = TConfig - - var lastCopiedStack: Pair<ItemStack, Text>? = null + var lastCopiedStack: Pair<ItemStack, Component>? = null set(value) { field = value - if (value != null) lastCopiedStackViewTime = true + if (value != null) lastCopiedStackViewTime = 2 } - var lastCopiedStackViewTime = false + var lastCopiedStackViewTime = 0 @Subscribe fun resetLastCopiedStack(event: TickEvent) { - if (!lastCopiedStackViewTime) lastCopiedStack = null - lastCopiedStackViewTime = false + if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null } @Subscribe @@ -71,39 +87,58 @@ object PowerUserTools : FirmamentFeature { lastCopiedStack = null } - fun debugFormat(itemStack: ItemStack): Text { - return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + fun debugFormat(itemStack: ItemStack): Component { + return Component.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + } + + @Subscribe + fun onRender(event: SlotRenderEvents.After) { + if (TConfig.showSlotNumbers.isPressed()) { + event.context.drawString( + MC.font, + event.slot.index.toString(), event.slot.x, event.slot.y, 0xFF00FF00.toInt(), true + ) + event.context.drawString( + MC.font, + event.slot.containerSlot.toString(), event.slot.x, event.slot.y + MC.font.lineHeight, 0xFFFF0000.toInt(), true + ) + } } @Subscribe fun onEntityInfo(event: WorldKeyboardEvent) { if (!event.matches(TConfig.copyEntityData)) return - val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity + val target = (MC.instance.hitResult as? EntityHitResult)?.entity if (target == null) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.fail")) + MC.sendChat(Component.translatable("firmament.poweruser.entity.fail")) return } showEntity(target) } fun showEntity(target: Entity) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) - MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) - MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) + val nbt = NbtPredicate.getEntityTagToCompare(target) + nbt.remove("Inventory") + nbt.put("StyledName", ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, target.feedbackDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.type", target.type)) + MC.sendChat(Component.translatable("firmament.poweruser.entity.name", target.name)) + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.position", target.position)) if (target is LivingEntity) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) - for (armorItem in target.armorItems) { - MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor")) + for ((slot, armorItem) in target.iterableArmorItems) { + MC.sendChat(Component.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) } } - MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size)) - target.passengerList.forEach { + MC.sendChat(Component.translatableEscape("firmament.poweruser.entity.passengers", target.passengers.size)) + target.passengers.forEach { showEntity(it) } } // TODO: leak this through some other way, maybe. - lateinit var getSkullId: (profile: ProfileComponent) -> Identifier? + lateinit var getSkullId: (profile: ResolvableProfile) -> ResourceLocation? @Subscribe fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { @@ -112,58 +147,74 @@ object PowerUserTools : FirmamentFeature { if (it.matches(TConfig.copyItemId)) { val sbId = item.skyBlockId if (sbId == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skyblockid.fail")) return } ClipboardUtils.setTextContent(sbId.neuItem) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.skyblockid", sbId.neuItem)) } else if (it.matches(TConfig.copyTexturePackId)) { - val model = CustomItemModelEvent.getModelIdentifier(item) // TODO: remove global texture overrides, maybe + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: ResourceLocation): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe if (model == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.modelid.fail")) return } ClipboardUtils.setTextContent(model.toString()) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.modelid", model.toString())) } else if (it.matches(TConfig.copyNbtData)) { // TODO: copy full nbt - val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "<empty>" + val nbt = item.get(DataComponents.CUSTOM_DATA)?.unsafeNbt?.toPrettyString() ?: "<empty>" ClipboardUtils.setTextContent(nbt) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.nbt")) } else if (it.matches(TConfig.copyLoreData)) { val list = mutableListOf(item.displayNameAccordingToNbt) list.addAll(item.loreAccordingToNbt) ClipboardUtils.setTextContent(list.joinToString("\n") { - TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() + ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() }) - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.lore")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.lore")) } else if (it.matches(TConfig.copySkullTexture)) { if (item.item != Items.PLAYER_HEAD) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) return } - val profile = item.get(DataComponentTypes.PROFILE) + val profile = item.get(DataComponents.PROFILE) if (profile == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) return } val skullTexture = getSkullId(profile) if (skullTexture == null) { - lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) + lastCopiedStack = Pair(item, Component.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) return } ClipboardUtils.setTextContent(skullTexture.toString()) lastCopiedStack = - Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())) + Pair(item, Component.translatableEscape("firmament.tooltip.copied.skull-id", skullTexture.toString())) println("Copied skull id: $skullTexture") } else if (it.matches(TConfig.copyItemStack)) { - ClipboardUtils.setTextContent( - ItemStack.CODEC - .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) - .orThrow.toPrettyString()) - lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.createSerializationContext(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) + lastCopiedStack = Pair(item, Component.translatableEscape("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle) && it.screen is AbstractContainerScreen<*>) { + val allTitles = ListTag() + val inventoryNames = + it.screen.menu.slots + .mapNotNullTo(mutableSetOf()) { it.container } + .filterIsInstance<Nameable>() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(ComponentSerialization.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) } } @@ -171,23 +222,23 @@ object PowerUserTools : FirmamentFeature { fun onCopyWorldInfo(it: WorldKeyboardEvent) { if (it.matches(TConfig.copySkullTexture)) { val p = MC.camera ?: return - val blockHit = p.raycast(20.0, 0.0f, false) ?: return + val blockHit = p.pick(20.0, 0.0f, false) ?: return if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) return } - val blockAt = p.world.getBlockState(blockHit.blockPos)?.block - val entity = p.world.getBlockEntity(blockHit.blockPos) - if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + val blockAt = p.level.getBlockState(blockHit.blockPos)?.block + val entity = p.level.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.ownerProfile == null) { + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) return } - val id = getSkullId(entity.owner!!) + val id = getSkullId(entity.ownerProfile!!) if (id == null) { - MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + MC.sendChat(Component.translatable("firmament.tooltip.copied.skull.fail")) } else { ClipboardUtils.setTextContent(id.toString()) - MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString())) + MC.sendChat(Component.translatableEscape("firmament.tooltip.copied.skull", id.toString())) } } } @@ -196,14 +247,14 @@ object PowerUserTools : FirmamentFeature { fun addItemId(it: ItemTooltipEvent) { if (TConfig.showItemIds) { val id = it.stack.skyBlockId ?: return - it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem)) + it.lines.add(Component.translatableEscape("firmament.tooltip.skyblockid", id.neuItem).grey()) } val (item, text) = lastCopiedStack ?: return - if (!ItemStack.areEqual(item, it.stack)) { + if (!ItemStack.matches(item, it.stack)) { lastCopiedStack = null return } - lastCopiedStackViewTime = true + lastCopiedStackViewTime = 0 it.lines.add(text) } diff --git a/src/main/kotlin/features/debug/SkinPreviews.kt b/src/main/kotlin/features/debug/SkinPreviews.kt new file mode 100644 index 0000000..56c63db --- /dev/null +++ b/src/main/kotlin/features/debug/SkinPreviews.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.features.debug + +import com.mojang.authlib.GameProfile +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.time.Duration.Companion.seconds +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.phys.Vec3 +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.IsSlotProtectedEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.rawSkyBlockId +import moe.nea.firmament.util.toTicks +import moe.nea.firmament.util.tr + + +object SkinPreviews { + + // TODO: add pet support + @Subscribe + fun onEntityUpdate(event: EntityUpdateEvent) { + if (!isRecording) return + if (event.entity.position != pos) + return + val entity = event.entity as? LivingEntity ?: return + val stack = entity.getItemBySlot(EquipmentSlot.HEAD) ?: return + val profile = stack.get(DataComponents.PROFILE)?.partialProfile() ?: return + if (profile == animation.lastOrNull()) return + animation.add(profile) + val shortened = animation.shortenCycle() + if (shortened.size <= (animation.size / 2).coerceAtLeast(1) && lastDiscard.passedTime() > 2.seconds) { + val tickEstimation = (lastDiscard.passedTime() / animation.size).toTicks() + val skinName = if (skinColor != null) "${skinId}_${skinColor?.replace(" ", "_")?.uppercase()}" else skinId!! + val json = + buildJsonObject { + put("ticks", tickEstimation) + put( + "textures", + shortened.map { + it.id.toString() + ":" + it.properties()["textures"].first().value() + }.toJsonArray() + ) + } + MC.sendChat( + tr( + "firmament.dev.skinpreviews.done", + "Observed a total of ${animation.size} elements, which could be shortened to a cycle of ${shortened.size}. Copying JSON array. Estimated ticks per frame: $tickEstimation." + ) + ) + isRecording = false + ClipboardUtils.setTextContent(JsonPrimitive(skinName).toString() + ":" + json.toString()) + } + } + + var animation = mutableListOf<GameProfile>() + var pos = Vec3(-1.0, 72.0, -101.25) + var isRecording = false + var skinColor: String? = null + var skinId: String? = null + var lastDiscard = TimeMark.farPast() + + @Subscribe + fun onActivate(event: IsSlotProtectedEvent) { + if (!PowerUserTools.TConfig.autoCopyAnimatedSkins) return + val lastLine = event.itemStack.loreAccordingToNbt.lastOrNull()?.string + if (lastLine != "Right-click to preview!" && lastLine != "Click to preview!") return + lastDiscard = TimeMark.now() + val stackName = event.itemStack.displayNameAccordingToNbt.string + if (stackName == "FIRE SALE!") { + skinColor = null + skinId = event.itemStack.rawSkyBlockId + } else { + skinColor = stackName + } + animation.clear() + isRecording = true + MC.sendChat(tr("firmament.dev.skinpreviews.start", "Starting to observe items")) + } +} diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt new file mode 100644 index 0000000..37b248a --- /dev/null +++ b/src/main/kotlin/features/debug/SoundVisualizer.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.network.chat.Component +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.red +import moe.nea.firmament.util.render.RenderInWorldContext + +object SoundVisualizer { + + var showSounds = false + + var sounds = mutableListOf<SoundReceiveEvent>() + + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("sounds") { + thenExecute { + showSounds = !showSounds + if (!showSounds) { + sounds.clear() + } + } + } + } + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + sounds.clear() + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + RenderInWorldContext.renderInWorld(event) { + sounds.forEach { event -> + withFacingThePlayer(event.position) { + text( + Component.literal(event.sound.value().location.toString()).also { + if (event.cancelled) + it.red() + }, + verticalAlign = RenderInWorldContext.VerticalAlign.CENTER, + ) + } + } + } + } + + @Subscribe + fun onSoundReceive(event: SoundReceiveEvent) { + if (!showSounds) return + if (sounds.size > 1000) { + sounds.subList(0, 200).clear() + } + sounds.add(event) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt new file mode 100644 index 0000000..d12d667 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt @@ -0,0 +1,256 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import net.minecraft.client.player.AbstractClientPlayer +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.core.ClientAsset +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.async.waitForTextInput +import moe.nea.firmament.util.ifDropLast +import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.setSkullOwner +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.red +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object ExportRecipe { + + + val xNames = "123" + val yNames = "ABC" + + val slotIndices = (0..<9).map { + val x = it % 3 + val y = it / 3 + + (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10 + } + val resultSlot = 25 + val craftingTableSlut = resultSlot - 2 + + @Subscribe + fun exportNpcLocation(event: WorldKeyboardEvent) { + if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) { + return + } + val entity = MC.instance.crosshairPickEntity + if (entity == null) { + MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export")) + return + } + Firmament.coroutineScope.launch { + val guessName = entity.level.getEntitiesOfClass( + ArmorStand::class.java, + entity.boundingBox.inflate(0.1), + { !it.name.string.contains("CLICK") }) + .firstOrNull()?.customName?.string + ?: "" + val reply = waitForTextInput("$guessName (NPC)", "Export stub") + val id = generateName(reply) + ItemExporter.exportStub(id, "§9$reply") { + val playerEntity = entity as? AbstractClientPlayer + val textureUrl = (playerEntity?.skin?.body as? ClientAsset.DownloadedTexture)?.url + if (textureUrl != null) + it.setSkullOwner(playerEntity.uuid, textureUrl) + } + ItemExporter.modifyJson(id) { + val mutJson = it.toMutableMap() + mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown") + mutJson["x"] = JsonPrimitive(entity.blockX) + mutJson["y"] = JsonPrimitive(entity.blockY) + mutJson["z"] = JsonPrimitive(entity.blockZ) + JsonObject(mutJson) + } + } + } + + @Subscribe + fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) { + if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) { + return + } + val title = event.screen.title.string + val sellSlot = event.screen.getSlotByIndex(49, false)?.item + val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false) + if (craftingTableSlot?.item?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") { + slotIndices.forEach { (_, index) -> + event.screen.getSlotByIndex(index, false)?.item?.let(ItemExporter::ensureExported) + } + val inputs = slotIndices.associate { (name, index) -> + val id = event.screen.getSlotByIndex(index, false)?.item?.takeIf { !it.isEmpty() }?.let { + "${it.skyBlockId?.neuItem}:${it.count}" + } ?: "" + name to JsonPrimitive(id) + } + val output = event.screen.getSlotByIndex(resultSlot, false)?.item!! + val overrideOutputId = output.skyBlockId!!.neuItem + val count = output.count + val recipe = JsonObject( + inputs + mapOf( + "type" to JsonPrimitive("crafting"), + "count" to JsonPrimitive(count), + "overrideOutputId" to JsonPrimitive(overrideOutputId) + ) + ) + ItemExporter.appendRecipe(output.skyBlockId!!, recipe) + MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported.")) + return + } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt + ?: listOf()).any { it.string == "Click to buyback!" } + ) { + val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC") + if (!ItemExporter.isExported(shopId)) { + // TODO: export location + skin of last clicked npc + ItemExporter.exportStub(shopId, "§9$title (NPC)") + } + for (index in (9..9 * 5)) { + val item = event.screen.getSlotByIndex(index, false)?.item ?: continue + val skyblockId = item.skyBlockId ?: continue + val costLines = item.loreAccordingToNbt + .map { it.string.trim() } + .dropWhile { !it.startsWith("Cost") } + .dropWhile { it == "Cost" } + .takeWhile { it != "Click to trade!" } + .takeWhile { it != "Stock" } + .filter { !it.isBlank() } + .map { it.removePrefix("Cost: ") } + + + val costs = costLines.mapNotNull { lineText -> + val line = findStackableItemByName(lineText) + if (line == null) { + MC.sendChat( + tr( + "firmament.repo.itemshop.fail", + "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}" + ).red() + ) + } + line + } + + + ItemExporter.appendRecipe( + shopId, JsonObject( + mapOf( + "type" to JsonPrimitive("npc_shop"), + "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }), + "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"), + ) + ) + ) + } + MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete.")) + } else { + MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found")) + } + } + + private val coinRegex = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern() + private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern() + private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern() + private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern() + + private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".toPattern() + + fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? { + var id = ItemNameLookup.guessItemByName(name, true) + if (id == null && fallbackToGenerated) { + id = generateName(name) + } + return id + } + + fun skill(name: String): SkyblockId { + return SkyblockId("SKYBLOCK_SKILL_${name}") + } + + fun generateName(name: String): SkyblockId { + return SkyblockId(name.uppercase().replace(" ", "_").replace(Regex("[^A-Z_]+"), "")) + } + + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? { + val properName = name.removeColorCodes().trim() + if (properName == "FREE" || properName == "This Chest is Free!") { + return Pair(SkyBlockItems.COINS, 0.0) + } + coinRegex.useMatch(properName) { + return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount"))) + } + etherialRewardPattern.useMatch(properName) { + val id = when (val id = group("what")) { + "Copper" -> SkyblockId("SKYBLOCK_COPPER") + "Bits" -> SkyblockId("SKYBLOCK_BIT") + "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN") + "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING") + "Gold Essence" -> SkyblockId("ESSENCE_GOLD") + "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE") + "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL") + "Pelts" -> SkyblockId("SKYBLOCK_PELT") + "Fine Flour" -> SkyblockId("FINE_FLOUR") + else -> { + id.ifDropLast(" Experience") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" XP") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" Powder") { + SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}") + } ?: id.ifDropLast(" Essence") { + SkyblockId("ESSENCE_${generateName(it).neuItem}") + } ?: generateName(id) + } + } + return Pair(id, parseShortNumber(group("amount"))) + } + essenceRegex.useMatch(properName) { + return Pair( + SkyblockId("ESSENCE_${group("essence").uppercase()}"), + parseShortNumber(group("count")) + ) + } + stackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + reverseStackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + numberedItemRegex.useMatch(properName) { + val item = findForName(group("what"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + + return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) } + } + +} diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt new file mode 100644 index 0000000..f664da3 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt @@ -0,0 +1,250 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.notExists +import kotlin.io.path.readText +import kotlin.io.path.relativeTo +import kotlin.io.path.writeText +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.StringTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.RepoDownloadManager +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.LegacyTagParser +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.setSkyBlockId +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr + +object ItemExporter { + + fun exportItem(itemStack: ItemStack): Component { + nonOverlayCache.clear() + val exporter = LegacyItemExporter.createExporter(itemStack) + var json = exporter.exportJson() + val fileName = json.jsonObject["internalname"]?.jsonPrimitive?.takeIf { it.isString }?.content + if (fileName == null) { + return tr( + "firmament.repoexport.nointernalname", + "Could not find internal name to export for this item (null.json)" + ) + } + val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json") + itemFile.createParentDirectories() + if (itemFile.exists()) { + val existing = try { + Firmament.json.decodeFromString<JsonObject>(itemFile.readText()) + } catch (ex: Exception) { + ex.printStackTrace() + JsonObject(mapOf()) + } + val mut = json.jsonObject.toMutableMap() + for (prop in existing) { + if (prop.key !in mut || mut[prop.key]!!.let { + (it is JsonPrimitive && (it.content.isEmpty() || it.content == "0")) || (it is JsonArray && it.isEmpty()) || (it is JsonObject && it.isEmpty()) + }) + mut[prop.key] = prop.value + } + json = JsonObject(mut) + } + val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json) + itemFile.writeText(jsonFormatted) + val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${fileName}.snbt") + overlayFile.createParentDirectories() + overlayFile.writeText(exporter.exportModernSnbt().toPrettyString()) + return tr( + "firmament.repoexport.success", + "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${ + exporter.warnings.joinToString( + "" + ) { "\nWarning: $it" } + }" + ) + } + + fun pathFor(skyBlockId: SkyblockId) = + RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json") + + fun isExported(skyblockId: SkyblockId) = + pathFor(skyblockId).exists() + + fun ensureExported(itemStack: ItemStack) { + if (!isExported(itemStack.skyBlockId ?: return)) + MC.sendChat(exportItem(itemStack)) + } + + fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) { + val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText()) + val newJson = modify(oldJson) + pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson))) + } + + fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) { + modifyJson(skyblockId) { oldJson -> + val mutableJson = oldJson.toMutableMap() + val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList() + recipes.add(recipe) + mutableJson["recipes"] = JsonArray(recipes) + JsonObject(mutableJson) + } + } + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("reexportlore") { + thenArgument("itemid", RestArgumentType) { itemid -> + suggests { ctx, builder -> + val spaceIndex = builder.remaining.lastIndexOf(" ") + val (before, after) = + if (spaceIndex < 0) Pair("", builder.remaining) + else Pair( + builder.remaining.substring(0, spaceIndex + 1), + builder.remaining.substring(spaceIndex + 1) + ) + RepoManager.neuRepo.items.items.keys + .asSequence() + .filter { it.startsWith(after, ignoreCase = true) } + .forEach { + builder.suggest(before + it) + } + + builder.buildFuture() + } + thenExecute { + for (itemid in get(itemid).split(" ").map { SkyblockId(it) }) { + if (pathFor(itemid).notExists()) { + MC.sendChat( + tr( + "firmament.repo.export.relore.fail", + "Could not find json file to relore for ${itemid}" + ) + ) + } + fixLoreNbtFor(itemid) + MC.sendChat( + tr( + "firmament.repo.export.relore", + "Updated lore / display name for $itemid" + ) + ) + } + } + } + thenLiteral("all") { + thenExecute { + var i = 0 + val chunkSize = 100 + val items = RepoManager.neuRepo.items.items.keys + Firmament.coroutineScope.launch { + items.chunked(chunkSize).forEach { key -> + MC.sendChat( + tr( + "firmament.repo.export.relore.progress", + "Updated lore / display for ${i * chunkSize} / ${items.size}." + ) + ) + i++ + key.forEach { + fixLoreNbtFor(SkyblockId(it)) + } + } + MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated.")) + } + } + } + } + } + } + + fun fixLoreNbtFor(itemid: SkyblockId) { + modifyJson(itemid) { + val mutJson = it.toMutableMap() + val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content) + val display = legacyTag.getCompoundOrEmpty("display") + legacyTag.put("display", display) + display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content) + display.put( + "Lore", + (mutJson["lore"] as JsonArray).map { StringTag.valueOf(it.jsonPrimitive.content) } + .toNbtList() + ) + mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString()) + JsonObject(mutJson) + } + } + + @Subscribe + fun onKeyBind(event: HandledScreenKeyPressedEvent) { + if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) { + val itemStack = event.screen.focusedItemStack ?: return + PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack)) + } + } + + val nonOverlayCache = mutableMapOf<SkyblockId, Boolean>() + + @Subscribe + fun onRender(event: SlotRenderEvents.Before) { + if (!PowerUserTools.TConfig.highlightNonOverlayItems) { + return + } + val stack = event.slot.item ?: return + val id = event.slot.item.skyBlockId?.neuItem + if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return + val sbId = stack.skyBlockId ?: return + val isExported = nonOverlayCache.getOrPut(sbId) { + RepoManager.overlayData.getOverlayFiles(sbId).isNotEmpty() || // This extra case is here so that an export works immediately, without repo reload + RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${stack.skyBlockId}.snbt") + .exists() + } + if (!isExported) + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } + + fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) { + exportItem(ItemStack(Items.PLAYER_HEAD).also { + it.displayNameAccordingToNbt = Component.literal(title) + it.loreAccordingToNbt = listOf(Component.literal("")) + it.setSkyBlockId(skyblockId) + extra(it) // LOL + }) + MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId")) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt new file mode 100644 index 0000000..2bc51bc --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt @@ -0,0 +1,87 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.Serializable +import net.minecraft.nbt.CompoundTag +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.util.StringUtil.camelWords +import moe.nea.firmament.util.mc.loadItemFromNbt + +/** + * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json) + */ +object LegacyItemData { + @Serializable + data class ItemData( + val id: Int, + val name: String, + val displayName: String, + val stackSize: Int, + val variations: List<Variation> = listOf() + ) { + val properId = if (name.contains(":")) name else "minecraft:$name" + + fun allVariants() = + variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0) + } + + @Serializable + data class Variation( + val metadata: Int, val displayName: String + ) + + data class LegacyItemType( + val name: String, + val metadata: Short + ) { + override fun toString(): String { + return "$name:$metadata" + } + } + + @Serializable + data class EnchantmentData( + val id: Int, + val name: String, + val displayName: String, + ) + + inline fun <reified T : Any> getLegacyData(name: String) = + Firmament.tryDecodeJsonFromStream<T>( + LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!! + ).getOrThrow() + + val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments") + val enchantmentLut = enchantmentData.associateBy { ResourceLocation.withDefaultNamespace(it.name) } + + val itemDat = getLegacyData<List<ItemData>>("items") + + @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread. + val itemLut = itemDat.flatMap { item -> + item.allVariants().map { legacyItemType -> + val nbt = ItemCache.convert189ToModern(CompoundTag().apply { + putString("id", legacyItemType.name) + putByte("Count", 1) + putShort("Damage", legacyItemType.metadata) + })!! + nbt.remove("components") + val stack = loadItemFromNbt(nbt) ?: error("Could not transform $legacyItemType: $nbt") + stack.item to legacyItemType + } + }.toMap() + + @Serializable + data class LegacyEffect( + val id: Int, + val name: String, + val displayName: String, + val type: String + ) + + val effectList = getLegacyData<List<LegacyEffect>>("effects") + .associateBy { + it.name.camelWords().map { it.trim().lowercase() }.joinToString("_") + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt new file mode 100644 index 0000000..7b855d1 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt @@ -0,0 +1,318 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.concurrent.thread +import kotlin.jvm.optionals.getOrNull +import net.minecraft.core.component.DataComponents +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.StringTag +import net.minecraft.tags.ItemTags +import net.minecraft.network.chat.Component +import net.minecraft.util.Unit +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.HypixelPetInfo +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.StringUtil.words +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.getLegacyFormatString +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.modifyExtraAttributes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +class LegacyItemExporter private constructor(var itemStack: ItemStack) { + init { + require(!itemStack.isEmpty) + itemStack.count = 1 + } + + var lore = itemStack.loreAccordingToNbt + val originalId = itemStack.extraAttributes.getString("id") + var name = itemStack.displayNameAccordingToNbt + val extraAttribs = itemStack.extraAttributes.copy() + val legacyNbt = CompoundTag() + val warnings = mutableListOf<String>() + + // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so + + fun preprocess() { + // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui + extraAttribs.remove("timestamp") + extraAttribs.remove("uuid") + extraAttribs.remove("modifier") + extraAttribs.getString("petInfo").ifPresent { petInfoJson -> + var petInfo = Firmament.json.decodeFromString<HypixelPetInfo>(petInfoJson) + petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null) + extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo)) + } + itemStack.skyBlockId?.let { + extraAttribs.putString("id", it.neuItem) + } + trimLore() + itemStack.loreAccordingToNbt = itemStack.item.defaultInstance.loreAccordingToNbt + itemStack.remove(DataComponents.CUSTOM_NAME) + } + + fun trimLore() { + val rarityIdx = lore.indexOfLast { + val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull() + firstWordInLine?.let(Rarity::fromString) != null + } + if (rarityIdx >= 0) { + lore = lore.subList(0, rarityIdx + 1) + } + + trimStats() + + deleteLineUntilNextSpace { it.startsWith("Held Item: ") } + deleteLineUntilNextSpace { it.startsWith("Progress to Level ") } + deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") } + deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") } + collapseWhitespaces() + + name = name.transformEachRecursively { + var string = it.directLiteralStringContent ?: return@transformEachRecursively it + string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}") + Component.literal(string).setStyle(it.style) + } + + if (lore.isEmpty()) + lore = listOf(Component.empty()) + } + + private fun trimStats() { + val lore = this.lore.toMutableList() + for (index in lore.indices) { + val value = lore[index] + val statLine = SBItemStack.parseStatLine(value) + if (statLine == null) break + val v = value.copy() + require(value.directLiteralStringContent == "") + v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") } + val last = v.siblings.last() + v.siblings[v.siblings.lastIndex] = + Component.literal(last.directLiteralStringContent!!.trimEnd()) + .setStyle(last.style) + lore[index] = v + } + this.lore = lore + } + + fun collapseWhitespaces() { + lore = (listOf(null as Component?) + lore).zipWithNext() + .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() } + .map { it.second!! } + } + + fun deleteLineUntilNextSpace(search: (String) -> Boolean) { + val idx = lore.indexOfFirst { search(it.unformattedString) } + if (idx < 0) return + val l = lore.toMutableList() + val p = l.subList(idx, l.size) + val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() } + if (nextBlank < 0) + p.clear() + else + p.subList(0, nextBlank).clear() + lore = l + } + + fun processNbt() { + // TODO: calculate hideflags + legacyNbt.put("HideFlags", IntTag.valueOf(254)) + copyUnbreakable() + copyItemModel() + copyPotion() + copyExtraAttributes() + copyLegacySkullNbt() + copyDisplay() + copyColour() + copyEnchantments() + copyEnchantGlint() + // TODO: copyDisplay + } + + private fun copyPotion() { + val effects = itemStack.get(DataComponents.POTION_CONTENTS) ?: return + legacyNbt.put("CustomPotionEffects", ListTag().also { + effects.allEffects.forEach { effect -> + val effectId = effect.effect.unwrapKey().get().location().path + val duration = effect.duration + val legacyId = LegacyItemData.effectList[effectId]!! + + it.add(CompoundTag().apply { + put("Ambient", ByteTag.valueOf(false)) + put("Duration", IntTag.valueOf(duration)) + put("Id", ByteTag.valueOf(legacyId.id.toByte())) + put("Amplifier", ByteTag.valueOf(effect.amplifier.toByte())) + }) + } + }) + } + + fun CompoundTag.getOrPutCompound(name: String): CompoundTag { + val compound = getCompoundOrEmpty(name) + put(name, compound) + return compound + } + + private fun copyColour() { + if (!itemStack.`is`(ItemTags.DYEABLE)) { + itemStack.remove(DataComponents.DYED_COLOR) + return + } + val leatherTint = itemStack.componentsPatch.get(DataComponents.DYED_COLOR)?.getOrNull() ?: return + legacyNbt.getOrPutCompound("display").put("color", IntTag.valueOf(leatherTint.rgb)) + } + + private fun copyItemModel() { + val itemModel = itemStack.get(DataComponents.ITEM_MODEL) ?: return + legacyNbt.put("ItemModel", StringTag.valueOf(itemModel.toString())) + } + + private fun copyDisplay() { + legacyNbt.getOrPutCompound("display").apply { + put("Lore", lore.map { StringTag.valueOf(it.getLegacyFormatString(trimmed = true)) }.toNbtList()) + putString("Name", name.getLegacyFormatString(trimmed = true)) + } + } + + fun exportModernSnbt(): Tag { + val overlay = ItemStack.CODEC.encodeStart(MC.currentOrDefaultRegistryNbtOps, itemStack.copy().also { + it.modifyExtraAttributes { attribs -> + originalId.ifPresent { attribs.putString("id", it) } + } + }).orThrow + val overlayWithVersion = + ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay) + .orThrow + return overlayWithVersion + } + + fun prepare() { + preprocess() + processNbt() + itemStack.extraAttributes = extraAttribs + } + + fun exportJson(): JsonElement { + return buildJsonObject { + val (itemId, damage) = legacyifyItemStack() + put("itemid", itemId) + put("displayname", name.getLegacyFormatString(trimmed = true)) + put("nbttag", legacyNbt.toLegacyString()) + put("damage", damage) + put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray()) + val sbId = itemStack.skyBlockId + if (sbId == null) + warnings.add("Could not find skyblock id") + put("internalname", sbId?.neuItem) + put("clickcommand", "") + put("crafttext", "") + put("modver", "Firmament ${Firmament.version.friendlyString}") + put("infoType", "") + put("info", JsonArray(listOf())) + } + + } + + companion object { + fun createExporter(itemStack: ItemStack): LegacyItemExporter { + return LegacyItemExporter(itemStack.copy()).also { it.prepare() } + } + + @Subscribe + fun load(event: ClientStartedEvent) { + thread(start = true, name = "ItemExporter Meta Load Thread") { + LegacyItemData.itemLut + } + } + } + + fun copyEnchantGlint() { + if (itemStack.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) == true) { + val ench = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", ench) + } + } + + private fun copyUnbreakable() { + if (itemStack.get(DataComponents.UNBREAKABLE) == Unit.INSTANCE) { + legacyNbt.putBoolean("Unbreakable", true) + } + } + + fun copyEnchantments() { + val enchantments = itemStack.get(DataComponents.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return + val enchTag = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", enchTag) + enchantments.entrySet().forEach { entry -> + val id = entry.key.unwrapKey().get().location() + val legacyId = LegacyItemData.enchantmentLut[id] + if (legacyId == null) { + warnings.add("Could not find legacy enchantment id for ${id}") + return@forEach + } + enchTag.add(CompoundTag().apply { + putShort("lvl", entry.intValue.toShort()) + putShort( + "id", + legacyId.id.toShort() + ) + }) + } + } + + fun copyExtraAttributes() { + legacyNbt.put("ExtraAttributes", extraAttribs) + } + + fun copyLegacySkullNbt() { + val profile = itemStack.get(DataComponents.PROFILE) ?: return + legacyNbt.put("SkullOwner", CompoundTag().apply { + putString("Id", profile.partialProfile().id.toString()) + putBoolean("hypixelPopulated", true) + put("Properties", CompoundTag().apply { + profile.partialProfile().properties().forEach { prop, value -> + val list = getListOrEmpty(prop) + put(prop, list) + list.add(CompoundTag().apply { + value.signature?.let { + putString("Signature", it) + } + putString("Value", value.value) + putString("Name", value.name) + }) + } + }) + }) + } + + fun legacyifyItemStack(): LegacyItemData.LegacyItemType { + // TODO: add a default here + if (itemStack.item == Items.LINGERING_POTION || itemStack.item == Items.SPLASH_POTION) + return LegacyItemData.LegacyItemType("potion", 16384) + return LegacyItemData.itemLut[itemStack.item]!! + } +} |
