feat: Add item shop recipe exporter

This commit is contained in:
Linnea Gräf
2025-06-22 21:39:57 +02:00
parent 89047619c6
commit c74930d6cb
5 changed files with 201 additions and 24 deletions

View File

@@ -1,14 +1,26 @@
package moe.nea.firmament.features.debug.itemeditor
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyblockId
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.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.useMatch
object ExportRecipe {
@@ -29,29 +41,168 @@ object ExportRecipe {
if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) {
return
}
if (!event.screen.title.string.endsWith(" Recipe")) {
MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found"))
return
}
slotIndices.forEach { (_, index) ->
event.screen.getSlotByIndex(index, false)?.stack?.let(ItemExporter::ensureExported)
}
val inputs = slotIndices.associate { (name, index) ->
val id = event.screen.getSlotByIndex(index, false)?.stack?.takeIf { !it.isEmpty() }?.let {
"${it.skyBlockId?.neuItem}:${it.count}"
} ?: ""
name to JsonPrimitive(id)
}
val output = event.screen.getSlotByIndex(resultSlot, false)?.stack!!
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)
val title = event.screen.title.string
val sellSlot = event.screen.getSlotByIndex(49, false)?.stack
if (title.endsWith(" Recipe")) {
slotIndices.forEach { (_, index) ->
event.screen.getSlotByIndex(index, false)?.stack?.let(ItemExporter::ensureExported)
}
val inputs = slotIndices.associate { (name, index) ->
val id = event.screen.getSlotByIndex(index, false)?.stack?.takeIf { !it.isEmpty() }?.let {
"${it.skyBlockId?.neuItem}:${it.count}"
} ?: ""
name to JsonPrimitive(id)
}
val output = event.screen.getSlotByIndex(resultSlot, false)?.stack!!
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)
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, "$title (NPC)")
}
for (index in (9..9 * 5)) {
val item = event.screen.getSlotByIndex(index, false)?.stack ?: 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(" ", "_"))
}
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) }
}
}

View File

@@ -10,6 +10,7 @@ import kotlin.io.path.readText
import kotlin.io.path.relativeTo
import kotlin.io.path.writeText
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
@@ -18,9 +19,13 @@ 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.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.setSkyBlockId
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.tr
@@ -52,8 +57,11 @@ object ItemExporter {
fun pathFor(skyBlockId: SkyblockId) =
RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json")
fun isExported(skyblockId: SkyblockId) =
pathFor(skyblockId).exists()
fun ensureExported(itemStack: ItemStack) {
if (!pathFor(itemStack.skyBlockId ?: return).exists())
if (!isExported(itemStack.skyBlockId ?: return))
exportItem(itemStack)
}
@@ -73,4 +81,13 @@ object ItemExporter {
PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack))
}
}
fun exportStub(skyblockId: SkyblockId, title: String) {
exportItem(ItemStack(Items.PLAYER_HEAD).also {
it.displayNameAccordingToNbt = Text.literal(title)
it.loreAccordingToNbt = listOf(Text.literal(""))
it.setSkyBlockId(skyblockId)
})
MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId"))
}
}

View File

@@ -75,6 +75,7 @@ class LegacyItemExporter private constructor(var itemStack: ItemStack) {
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 {

View File

@@ -26,6 +26,13 @@ inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? {
?.let(block)
}
fun <T> String.ifDropLast(suffix: String, block: (String) -> T): T? {
if (endsWith(suffix)) {
return block(dropLast(suffix.length))
}
return null
}
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"

View File

@@ -3,6 +3,7 @@ package moe.nea.firmament.util.skyblock
import moe.nea.firmament.util.SkyblockId
object SkyBlockItems {
val COINS = SkyblockId("SKYBLOCK_COIN")
val ROTTEN_FLESH = SkyblockId("ROTTEN_FLESH")
val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND")
val DIAMOND = SkyblockId("DIAMOND")