diff options
author | Linnea Gräf <nea@nea.moe> | 2025-06-22 21:39:57 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2025-06-22 21:39:57 +0200 |
commit | c74930d6cb142cb8c3480a387645f8b4752c953a (patch) | |
tree | c240949e33312e71ee8799638838d022539234eb /src | |
parent | 89047619c62343b5f1fa26ef825595bcba02ad0a (diff) | |
download | Firmament-c74930d6cb142cb8c3480a387645f8b4752c953a.tar.gz Firmament-c74930d6cb142cb8c3480a387645f8b4752c953a.tar.bz2 Firmament-c74930d6cb142cb8c3480a387645f8b4752c953a.zip |
feat: Add item shop recipe exporter
Diffstat (limited to 'src')
5 files changed, 199 insertions, 22 deletions
diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt index 25e5cbd..75ab943 100644 --- a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt +++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt @@ -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")) + 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) + 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")) } - 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) + } + + 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")) ) - ) - ItemExporter.appendRecipe(output.skyBlockId!!, recipe) + } + 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 index 64e14f2..fd411c9 100644 --- a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt +++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt @@ -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")) + } } diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt index 1e9a4e9..fbb778a 100644 --- a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt @@ -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 { diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index f239810..be6bcfb 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -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]" diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index ca2b17b..74e1327 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -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") |