aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt6
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt147
-rw-r--r--src/main/kotlin/repo/BetterRepoRecipeCache.kt36
-rw-r--r--src/main/kotlin/repo/EssenceRecipeProvider.kt75
-rw-r--r--src/main/kotlin/repo/ExtraRecipeProvider.kt7
-rw-r--r--src/main/kotlin/repo/ItemCache.kt47
-rw-r--r--src/main/kotlin/repo/Reforge.kt146
-rw-r--r--src/main/kotlin/repo/ReforgeStore.kt124
-rw-r--r--src/main/kotlin/repo/RepoItemTypeCache.kt15
-rw-r--r--src/main/kotlin/repo/RepoManager.kt6
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt21
-rw-r--r--src/main/kotlin/util/LegacyFormattingCode.kt54
-rw-r--r--src/main/kotlin/util/SkyblockId.kt18
-rw-r--r--src/main/kotlin/util/skyblock/ItemType.kt27
-rw-r--r--src/main/kotlin/util/skyblock/Rarity.kt21
-rw-r--r--src/test/resources/testdata/items/hyperion.snbt96
-rw-r--r--src/test/resources/testdata/items/implosion-belt.snbt105
17 files changed, 858 insertions, 93 deletions
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
index f576eda..92f2cfc 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
@@ -24,6 +24,7 @@ import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe
import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe
import moe.nea.firmament.compat.rei.recipes.SBKatRecipe
import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe
+import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
@@ -78,6 +79,7 @@ class FirmamentReiPlugin : REIClientPlugin {
registry.add(SBForgeRecipe.Category)
registry.add(SBMobDropRecipe.Category)
registry.add(SBKatRecipe.Category)
+ registry.add(SBReforgeRecipe.Category)
registry.add(SBEssenceUpgradeRecipe.Category)
}
@@ -91,6 +93,10 @@ class FirmamentReiPlugin : REIClientPlugin {
SBCraftingRecipe.Category.catIdentifier,
SkyblockCraftingRecipeDynamicGenerator)
registry.registerDisplayGenerator(
+ SBReforgeRecipe.catIdentifier,
+ SBReforgeRecipe.DynamicGenerator
+ )
+ registry.registerDisplayGenerator(
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator)
registry.registerDisplayGenerator(
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
new file mode 100644
index 0000000..2805132
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
@@ -0,0 +1,147 @@
+package moe.nea.firmament.compat.rei.recipes
+
+import java.util.Optional
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.Renderer
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory
+import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator
+import me.shedaniel.rei.api.client.view.ViewSearchBuilder
+import me.shedaniel.rei.api.common.category.CategoryIdentifier
+import me.shedaniel.rei.api.common.display.Display
+import me.shedaniel.rei.api.common.display.DisplaySerializer
+import me.shedaniel.rei.api.common.entry.EntryIngredient
+import me.shedaniel.rei.api.common.entry.EntryStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.compat.rei.SBItemEntryDefinition
+import moe.nea.firmament.repo.Reforge
+import moe.nea.firmament.repo.ReforgeStore
+import moe.nea.firmament.repo.RepoItemTypeCache
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblockId
+import moe.nea.firmament.util.tr
+
+class SBReforgeRecipe(
+ val reforge: Reforge,
+ val limitToItem: SkyblockId?,
+) : Display {
+ companion object {
+ val catIdentifier = CategoryIdentifier.of<SBReforgeRecipe>(Firmament.MOD_ID, "reforge_recipe")
+ }
+
+ object Category : DisplayCategory<SBReforgeRecipe> {
+ override fun getCategoryIdentifier(): CategoryIdentifier<out SBReforgeRecipe> {
+ return catIdentifier
+ }
+
+ override fun getTitle(): Text {
+ return tr("firmament.recipecategory.reforge", "Reforge")
+ }
+
+ override fun getIcon(): Renderer {
+ return SBItemEntryDefinition.getEntry(SkyblockId("REFORGE_ANVIL"))
+ }
+
+ override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList<Widget> {
+ val list = mutableListOf<Widget>()
+ list.add(Widgets.createRecipeBase(bounds))
+ // TODO: actual layout after christmas, probably
+ list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY))
+ .markInput().entries(display.inputItems))
+ val stoneSlot = Widgets.createSlot(Point(bounds.minX + 38, bounds.centerY))
+ .markInput()
+ if (display.reforgeStone != null)
+ stoneSlot.entry(display.reforgeStone)
+ list.add(stoneSlot)
+ list.add(Widgets.createSlot(Point(bounds.minX + 38 + 18, bounds.centerY))
+ .markInput().entries(display.outputItems))
+ return list
+ }
+ }
+
+ object DynamicGenerator : DynamicDisplayGenerator<SBReforgeRecipe> {
+ fun getRecipesForSBItemStack(item: SBItemStack): Optional<List<SBReforgeRecipe>> {
+ val reforgeRecipes = mutableListOf<SBReforgeRecipe>()
+ for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) {
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ }
+ for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) {
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ }
+ if (reforgeRecipes.isEmpty()) return Optional.empty()
+ return Optional.of(reforgeRecipes)
+ }
+
+ override fun getRecipeFor(entry: EntryStack<*>): Optional<List<SBReforgeRecipe>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ return getRecipesForSBItemStack(item)
+ }
+
+ override fun getUsageFor(entry: EntryStack<*>): Optional<List<SBReforgeRecipe>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ ReforgeStore.byReforgeStone[item.skyblockId]?.let { stoneReforge ->
+ return Optional.of(listOf(SBReforgeRecipe(stoneReforge, null)))
+ }
+ return getRecipesForSBItemStack(item)
+ }
+
+ override fun generate(builder: ViewSearchBuilder): Optional<List<SBReforgeRecipe>> {
+ // TODO: check builder.recipesFor and such and optionally return all reforge recipes
+ return Optional.empty()
+ }
+ }
+
+ private val eligibleItems =
+ if (limitToItem != null) listOfNotNull(RepoManager.getNEUItem(limitToItem))
+ else reforge.eligibleItems.flatMap {
+ when (it) {
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName ->
+ listOfNotNull(RepoManager.getNEUItem(it.internalName))
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType ->
+ ReforgeStore.resolveItemType(it.itemType)
+ .flatMap {
+ RepoItemTypeCache.byItemType[it] ?: listOf()
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type
+ }
+ }
+ }
+ private val inputItems = eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) }
+ private val outputItems =
+ inputItems.map { SBItemEntryDefinition.getEntry(it.value.copy(reforge = reforge.reforgeId)) }
+ private val reforgeStone = reforge.reforgeStone?.let(SBItemEntryDefinition::getEntry)
+ private val inputEntries =
+ listOf(EntryIngredient.of(inputItems)) + listOfNotNull(reforgeStone?.let(EntryIngredient::of))
+ private val outputEntries = listOf(EntryIngredient.of(outputItems))
+
+ override fun getInputEntries(): List<EntryIngredient> {
+ return inputEntries
+ }
+
+ override fun getOutputEntries(): List<EntryIngredient> {
+ return outputEntries
+ }
+
+ override fun getCategoryIdentifier(): CategoryIdentifier<*> {
+ return catIdentifier
+ }
+
+ override fun getDisplayLocation(): Optional<Identifier> {
+ return Optional.empty()
+ }
+
+ override fun getSerializer(): DisplaySerializer<out Display>? {
+ return null
+ }
+}
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
index 91a6b50..4b32e57 100644
--- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt
+++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -6,23 +5,22 @@ import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
-class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) : IReloadable {
- var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
+ var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+ var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- override fun reload(repository: NEURepository) {
- val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val baseRecipes = repository.items.items.values
- .asSequence()
- .flatMap { it.recipes }
- val extraRecipes = essenceRecipeProvider.recipes
- (baseRecipes + extraRecipes)
- .forEach { recipe ->
- recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- }
- this.usages = usages
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val baseRecipes = repository.items.items.values
+ .asSequence()
+ .flatMap { it.recipes }
+ (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() })
+ .forEach { recipe ->
+ recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ }
+ this.usages = usages
+ this.recipes = recipes
+ }
}
diff --git a/src/main/kotlin/repo/EssenceRecipeProvider.kt b/src/main/kotlin/repo/EssenceRecipeProvider.kt
index 1833258..38559d5 100644
--- a/src/main/kotlin/repo/EssenceRecipeProvider.kt
+++ b/src/main/kotlin/repo/EssenceRecipeProvider.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -7,44 +6,46 @@ import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
-class EssenceRecipeProvider : IReloadable {
- data class EssenceUpgradeRecipe(
- val itemId: SkyblockId,
- val starCountAfter: Int,
- val essenceCost: Int,
- val essenceType: String, // TODO: replace with proper type
- val extraItems: List<NEUIngredient>,
- ) : NEURecipe {
- val essenceIngredient= NEUIngredient.fromString("${essenceType}:$essenceCost")
- val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider {
+ data class EssenceUpgradeRecipe(
+ val itemId: SkyblockId,
+ val starCountAfter: Int,
+ val essenceCost: Int,
+ val essenceType: String, // TODO: replace with proper type
+ val extraItems: List<NEUIngredient>,
+ ) : NEURecipe {
+ val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost")
+ val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+
+ override fun getAllInputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
+ }
- override fun getAllInputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
- }
+ override fun getAllOutputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
+ }
+ }
- override fun getAllOutputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
- }
- }
+ var recipes = listOf<EssenceUpgradeRecipe>()
+ private set
- var recipes = listOf<EssenceUpgradeRecipe>()
- private set
+ override fun provideExtraRecipes(): Iterable<NEURecipe> = recipes
- override fun reload(repository: NEURepository) {
- val recipes = mutableListOf<EssenceUpgradeRecipe>()
- for ((neuId, costs) in repository.constants.essenceCost.costs) {
- // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
- for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
- val items = costs.itemCosts[starCountAfter] ?: emptyList()
- recipes.add(
- EssenceUpgradeRecipe(
- SkyblockId(neuId),
- starCountAfter,
- essenceCost,
- "ESSENCE_" + costs.type.uppercase(), // how flimsy
- items.map { NEUIngredient.fromString(it) }))
- }
- }
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val recipes = mutableListOf<EssenceUpgradeRecipe>()
+ for ((neuId, costs) in repository.constants.essenceCost.costs) {
+ // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
+ for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
+ val items = costs.itemCosts[starCountAfter] ?: emptyList()
+ recipes.add(
+ EssenceUpgradeRecipe(
+ SkyblockId(neuId),
+ starCountAfter,
+ essenceCost,
+ "ESSENCE_" + costs.type.uppercase(), // how flimsy
+ items.map { NEUIngredient.fromString(it) }))
+ }
+ }
+ this.recipes = recipes
+ }
}
diff --git a/src/main/kotlin/repo/ExtraRecipeProvider.kt b/src/main/kotlin/repo/ExtraRecipeProvider.kt
new file mode 100644
index 0000000..9d3b5a0
--- /dev/null
+++ b/src/main/kotlin/repo/ExtraRecipeProvider.kt
@@ -0,0 +1,7 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.data.NEURecipe
+
+interface ExtraRecipeProvider {
+ fun provideExtraRecipes(): Iterable<NEURecipe>
+}
diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt
index 014de7d..f88dd48 100644
--- a/src/main/kotlin/repo/ItemCache.kt
+++ b/src/main/kotlin/repo/ItemCache.kt
@@ -24,21 +24,26 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtString
+import net.minecraft.text.Style
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.config.HudMeta
import moe.nea.firmament.gui.config.HudPosition
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.repo.RepoManager.initialize
+import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
+import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
import moe.nea.firmament.util.mc.appendLore
+import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.mc.modifyLore
import moe.nea.firmament.util.mc.setCustomName
import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.transformEachRecursively
object ItemCache : IReloadable {
private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
@@ -70,6 +75,7 @@ object ItemCache : IReloadable {
val ItemStack.isBroken
get() = get(FirmamentDataComponentTypes.IS_BROKEN) ?: false
+
fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack {
return ItemStack(Items.PAINTING).apply {
setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null"))
@@ -88,6 +94,35 @@ object ItemCache : IReloadable {
}
}
+ fun un189Lore(lore: String): Text {
+ val base = Text.literal("")
+ base.setStyle(Style.EMPTY.withItalic(false))
+ var lastColorCode = Style.EMPTY
+ var readOffset = 0
+ while (readOffset < lore.length) {
+ var nextCode = lore.indexOf('§', readOffset)
+ if (nextCode < 0) {
+ nextCode = lore.length
+ }
+ val text = lore.substring(readOffset, nextCode)
+ if (text.isNotEmpty()) {
+ base.append(Text.literal(text).setStyle(lastColorCode))
+ }
+ readOffset = nextCode + 2
+ if (nextCode + 1 < lore.length) {
+ val colorCode = lore[nextCode + 1]
+ val formatting = LegacyFormattingCode.byCode[colorCode.lowercaseChar()] ?: LegacyFormattingCode.RESET
+ val modernFormatting = formatting.modern
+ if (modernFormatting.isColor) {
+ lastColorCode = Style.EMPTY.withColor(modernFormatting)
+ } else {
+ lastColorCode = lastColorCode.withFormatting(modernFormatting)
+ }
+ }
+ }
+ return base
+ }
+
private fun NEUItem.asItemStackNow(): ItemStack {
try {
val oldItemTag = get10809CompoundTag()
@@ -95,6 +130,7 @@ object ItemCache : IReloadable {
?: return brokenItemStack(this)
val itemInstance =
ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
+ itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) }
val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
if (extraAttributes != null)
itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
@@ -129,12 +165,13 @@ object ItemCache : IReloadable {
}
fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text {
- assert(this.siblings.isEmpty())
- var string = this.string
- loreReplacements.forEach { (find, replace) ->
- string = string.replace("{$find}", replace)
+ return this.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ loreReplacements.forEach { (find, replace) ->
+ string = string.replace("{$find}", replace)
+ }
+ Text.literal(string).setStyle(it.style)
}
- return Text.literal(string).styled { this.style }
}
var job: Job? = null
diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt
new file mode 100644
index 0000000..ea01818
--- /dev/null
+++ b/src/main/kotlin/repo/Reforge.kt
@@ -0,0 +1,146 @@
+package moe.nea.firmament.repo
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblock.Rarity
+
+@Serializable
+data class Reforge(
+ val reforgeName: String,
+ @SerialName("internalName") val reforgeStone: SkyblockId? = null,
+ val nbtModifier: ReforgeId? = null,
+ val requiredRarities: List<Rarity>? = null,
+ val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List<ReforgeEligibilityFilter>? = null,
+ val allowOn: List<ReforgeEligibilityFilter>? = null,
+ val reforgeCosts: RarityMapped<Double>? = null,
+ val reforgeAbility: RarityMapped<String>? = null,
+ val reforgeStats: RarityMapped<Map<String, Double>>? = null,
+) {
+ val eligibleItems get() = allowOn ?: itemTypes ?: listOf()
+
+ @Serializable(with = ReforgeEligibilityFilter.Serializer::class)
+ sealed interface ReforgeEligibilityFilter {
+ object ItemTypesSerializer : KSerializer<List<ReforgeEligibilityFilter>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ override fun deserialize(decoder: Decoder): List<ReforgeEligibilityFilter> {
+ decoder as JsonDecoder
+ val jsonElement = decoder.decodeJsonElement()
+ if (jsonElement is JsonPrimitive && jsonElement.isString) {
+ return jsonElement.content.split("/").map { AllowsItemType(ItemType.ofName(it)) }
+ }
+ if (jsonElement is JsonArray) {
+ return decoder.json.decodeFromJsonElement(serializer<List<ReforgeEligibilityFilter>>(), jsonElement)
+ }
+ jsonElement as JsonObject
+ val filters = mutableListOf<ReforgeEligibilityFilter>()
+ jsonElement["internalName"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<SkyblockId>>(), it).forEach {
+ filters.add(AllowsInternalName(it))
+ }
+ }
+ jsonElement["itemId"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach {
+ val ident = Identifier.tryParse(it)
+ if (ident != null)
+ filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident)))
+ }
+ }
+ return filters
+ }
+
+ override fun serialize(encoder: Encoder, value: List<ReforgeEligibilityFilter>) {
+ TODO("Not yet implemented")
+ }
+ }
+
+ object Serializer : KSerializer<ReforgeEligibilityFilter> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter {
+ val jsonObject = serializer<JsonObject>().deserialize(decoder)
+ jsonObject["internalName"]?.let {
+ return AllowsInternalName(SkyblockId((it as JsonPrimitive).content))
+ }
+ jsonObject["itemType"]?.let {
+ return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content))
+ }
+ jsonObject["minecraftId"]?.let {
+ return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM,
+ Identifier.of((it as JsonPrimitive).content)))
+ }
+ error("Unknown item type")
+ }
+
+ override fun serialize(encoder: Encoder, value: ReforgeEligibilityFilter) {
+ TODO("Not yet implemented")
+ }
+
+ }
+
+ data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter
+ data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter
+ data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter
+ }
+
+
+ val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase())
+
+ @Serializable(with = RarityMapped.Serializer::class)
+ sealed interface RarityMapped<T> {
+ class Serializer<T>(
+ val values: KSerializer<T>
+ ) : KSerializer<RarityMapped<T>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ val indirect = MapSerializer(Rarity.serializer(), values)
+ override fun deserialize(decoder: Decoder): RarityMapped<T> {
+ decoder as JsonDecoder
+ val element = decoder.decodeJsonElement()
+ if (element is JsonObject) {
+ return PerRarity(decoder.json.decodeFromJsonElement(indirect, element))
+ } else {
+ return Direct(decoder.json.decodeFromJsonElement(values, element))
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: RarityMapped<T>) {
+ when (value) {
+ is Direct<T> ->
+ values.serialize(encoder, value.value)
+
+ is PerRarity<T> ->
+ indirect.serialize(encoder, value.values)
+ }
+ }
+ }
+
+ @Serializable
+ data class Direct<T>(val value: T) : RarityMapped<T>
+
+ @Serializable
+ data class PerRarity<T>(val values: Map<Rarity, T>) : RarityMapped<T>
+ }
+
+}
diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt
new file mode 100644
index 0000000..f03903b
--- /dev/null
+++ b/src/main/kotlin/repo/ReforgeStore.kt
@@ -0,0 +1,124 @@
+package moe.nea.firmament.repo
+
+import com.google.gson.JsonElement
+import com.mojang.serialization.JsonOps
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepoFile
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.NEURepositoryException
+import io.github.moulberry.repo.data.NEURecipe
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.json.KJsonOps
+import moe.nea.firmament.util.skyblock.ItemType
+
+object ReforgeStore : ExtraRecipeProvider, IReloadable {
+ override fun provideExtraRecipes(): Iterable<NEURecipe> {
+ return emptyList()
+ }
+
+ var byType: Map<ItemType, List<Reforge>> = mapOf()
+ var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf()
+ var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf()
+ var modifierLut = mapOf<ReforgeId, Reforge>()
+ var byReforgeStone = mapOf<SkyblockId, Reforge>()
+ var allReforges = listOf<Reforge>()
+
+ fun findEligibleForItem(itemType: ItemType): List<Reforge> {
+ return byType[itemType] ?: listOf()
+ }
+
+ fun findEligibleForInternalName(internalName: SkyblockId): List<Reforge> {
+ return byInternalName[internalName] ?: listOf()
+ }
+
+ //TODO: return byVanillla
+ override fun reload(repo: NEURepository) {
+ val basicReforges =
+ repo.file("constants/reforges.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val advancedReforges =
+ repo.file("constants/reforgestones.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val allReforges = (basicReforges + advancedReforges)
+ modifierLut = allReforges.associateBy { it.reforgeId }
+ byReforgeStone = allReforges.filter { it.reforgeStone != null }
+ .associateBy { it.reforgeStone!! }
+ val byType = mutableMapOf<ItemType, MutableList<Reforge>>()
+ val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>()
+ val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>()
+ this.byType = byType
+ this.byVanilla = byVanilla
+ this.byInternalName = byInternalName
+ for (reforge in allReforges) {
+ for (eligibleItem in reforge.eligibleItems) {
+ when (eligibleItem) {
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> {
+ byInternalName.getOrPut(eligibleItem.internalName, ::mutableListOf).add(reforge)
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType -> {
+ val actualItemTypes = resolveItemType(eligibleItem.itemType)
+ for (itemType in actualItemTypes) {
+ byType.getOrPut(itemType, ::mutableListOf).add(reforge)
+ }
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ byVanilla.getOrPut(eligibleItem.minecraftId, ::mutableListOf).add(reforge)
+ }
+ }
+ }
+ }
+ this.allReforges = allReforges
+ }
+
+ fun resolveItemType(itemType: ItemType): List<ItemType> {
+ if (ItemType.SWORD == itemType) {
+ return listOf(
+ ItemType.SWORD,
+ ItemType.GAUNTLET,
+ ItemType.LONGSWORD,// TODO: check name
+ ItemType.FISHING_WEAPON,// TODO: check name
+ )
+ }
+ if (itemType == ItemType.ofName("ARMOR")) {
+ return listOf(
+ ItemType.CHESTPLATE,
+ ItemType.LEGGINGS,
+ ItemType.HELMET,
+ ItemType.BOOTS,
+ )
+ }
+ if (itemType == ItemType.EQUIPMENT) {
+ return listOf(
+ ItemType.CLOAK,
+ ItemType.BRACELET,
+ ItemType.NECKLACE,
+ ItemType.BELT,
+ ItemType.GLOVES,
+ )
+ }
+ if (itemType == ItemType.ROD) {
+ return listOf(ItemType.FISHING_ROD, ItemType.FISHING_WEAPON)
+ }
+ return listOf(itemType)
+ }
+
+ fun <T> NEURepoFile.kJson(serializer: KSerializer<T>): T {
+ val rawJson = json(JsonElement::class.java)
+ try {
+ val kJsonElement = JsonOps.INSTANCE.convertTo(KJsonOps.INSTANCE, rawJson)
+ return Firmament.json.decodeFromJsonElement(serializer, kJsonElement)
+ } catch (ex: Exception) {
+ throw NEURepositoryException(path, "Could not decode kotlin JSON element", ex)
+ }
+ }
+}
diff --git a/src/main/kotlin/repo/RepoItemTypeCache.kt b/src/main/kotlin/repo/RepoItemTypeCache.kt
new file mode 100644
index 0000000..414ec09
--- /dev/null
+++ b/src/main/kotlin/repo/RepoItemTypeCache.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUItem
+import moe.nea.firmament.util.skyblock.ItemType
+
+object RepoItemTypeCache : IReloadable {
+
+ var byItemType: Map<ItemType?, List<NEUItem>> = mapOf()
+
+ override fun reload(repository: NEURepository) {
+ byItemType = repository.items.items.values.groupBy { ItemType.fromEscapeCodeLore(it.lore.lastOrNull() ?: "") }
+ }
+}
diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt
index 667ab73..6d9ba14 100644
--- a/src/main/kotlin/repo/RepoManager.kt
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -53,13 +53,16 @@ object RepoManager {
var recentlyFailedToUpdateItemList = false
val essenceRecipeProvider = EssenceRecipeProvider()
- val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider)
+ val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore)
fun makeNEURepository(path: Path): NEURepository {
return NEURepository.of(path).apply {
registerReloadListener(ItemCache)
+ registerReloadListener(RepoItemTypeCache)
registerReloadListener(ExpLadders)
registerReloadListener(ItemNameLookup)
+ registerReloadListener(ReforgeStore)
+ registerReloadListener(essenceRecipeProvider)
ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this))
registerReloadListener {
if (TestUtil.isInTest) return@registerReloadListener
@@ -70,7 +73,6 @@ object RepoManager {
}
}
}
- registerReloadListener(essenceRecipeProvider)
registerReloadListener(recipeCache)
}
}
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 18126ee..e5cacaa 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -14,11 +14,16 @@ import net.minecraft.util.Formatting
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.ReforgeId
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.getReforgeId
+import moe.nea.firmament.util.getUpgradeStars
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.withColor
@@ -28,8 +33,8 @@ data class SBItemStack constructor(
private var stackSize: Int,
private var petData: PetData?,
val extraLore: List<Text> = emptyList(),
- // TODO: grab this star data from nbt if possible
val stars: Int = 0,
+ val reforge: ReforgeId? = null,
) {
fun getStackSize() = stackSize
@@ -66,7 +71,9 @@ data class SBItemStack constructor(
skyblockId,
RepoManager.getNEUItem(skyblockId),
itemStack.count,
- petData = itemStack.petData?.let { PetData.fromHypixel(it) }
+ petData = itemStack.petData?.let { PetData.fromHypixel(it) },
+ stars = itemStack.getUpgradeStars(),
+ reforge = itemStack.getReforgeId()
)
}
@@ -127,6 +134,15 @@ data class SBItemStack constructor(
}
+ private fun appendReforgeStatsToLore(
+ itemStack: ItemStack,
+ ) {
+ val rarity = itemStack.rarity
+ val lore = itemStack.loreAccordingToNbt
+ }
+
+ // TODO: avoid instantiating the item stack here
+ val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack())
private var itemStack_: ItemStack? = null
private val itemStack: ItemStack
@@ -141,6 +157,7 @@ data class SBItemStack constructor(
return@run neuItem.asItemStack(idHint = skyblockId, replacementData)
.copyWithCount(stackSize)
.also { it.appendLore(extraLore) }
+ .also { if (reforge != null) it.appendLore(listOf(Text.literal("Reforge: $reforge"))) } // TODO: use this for proper rendering
.also { enhanceStatsByStars(it, stars) }
}
if (itemStack_ == null)
diff --git a/src/main/kotlin/util/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt
index 44bacfc..1a5d1dd 100644
--- a/src/main/kotlin/util/LegacyFormattingCode.kt
+++ b/src/main/kotlin/util/LegacyFormattingCode.kt
@@ -1,35 +1,37 @@
-
-
package moe.nea.firmament.util
import net.minecraft.util.Formatting
enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) {
- BLACK("BLACK", '0', 0),
- DARK_BLUE("DARK_BLUE", '1', 1),
- DARK_GREEN("DARK_GREEN", '2', 2),
- DARK_AQUA("DARK_AQUA", '3', 3),
- DARK_RED("DARK_RED", '4', 4),
- DARK_PURPLE("DARK_PURPLE", '5', 5),
- GOLD("GOLD", '6', 6),
- GRAY("GRAY", '7', 7),
- DARK_GRAY("DARK_GRAY", '8', 8),
- BLUE("BLUE", '9', 9),
- GREEN("GREEN", 'a', 10),
- AQUA("AQUA", 'b', 11),
- RED("RED", 'c', 12),
- LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
- YELLOW("YELLOW", 'e', 14),
- WHITE("WHITE", 'f', 15),
- OBFUSCATED("OBFUSCATED", 'k', -1),
- BOLD("BOLD", 'l', -1),
- STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
- UNDERLINE("UNDERLINE", 'n', -1),
- ITALIC("ITALIC", 'o', -1),
- RESET("RESET", 'r', -1);
+ BLACK("BLACK", '0', 0),
+ DARK_BLUE("DARK_BLUE", '1', 1),
+ DARK_GREEN("DARK_GREEN", '2', 2),
+ DARK_AQUA("DARK_AQUA", '3', 3),
+ DARK_RED("DARK_RED", '4', 4),
+ DARK_PURPLE("DARK_PURPLE", '5', 5),
+ GOLD("GOLD", '6', 6),
+ GRAY("GRAY", '7', 7),
+ DARK_GRAY("DARK_GRAY", '8', 8),
+ BLUE("BLUE", '9', 9),
+ GREEN("GREEN", 'a', 10),
+ AQUA("AQUA", 'b', 11),
+ RED("RED", 'c', 12),
+ LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
+ YELLOW("YELLOW", 'e', 14),
+ WHITE("WHITE", 'f', 15),
+ OBFUSCATED("OBFUSCATED", 'k', -1),
+ BOLD("BOLD", 'l', -1),
+ STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
+ UNDERLINE("UNDERLINE", 'n', -1),
+ ITALIC("ITALIC", 'o', -1),
+ RESET("RESET", 'r', -1);
+
+ companion object {
+ val byCode = entries.associateBy { it.char }
+ }
- val modern = Formatting.byCode(char)!!
+ val modern = Formatting.byCode(char)!!
- val formattingCode = "§$char"
+ val formattingCode = "§$char"
}
diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
index 1c1aa77..497a2d2 100644
--- a/src/main/kotlin/util/SkyblockId.kt
+++ b/src/main/kotlin/util/SkyblockId.kt
@@ -125,10 +125,26 @@ val ItemStack.skyblockUUID: UUID?
private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") {
val jsonString = it.extraAttributes.getString("petInfo")
if (jsonString.isNullOrBlank()) return@memoize Optional.empty()
- ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
+ ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") {
+ jsonparser.decodeFromString<HypixelPetInfo>(jsonString)
+ }
.or { null }.intoOptional()
}
+fun ItemStack.getUpgradeStars(): Int {
+ return extraAttributes.getInt("upgrade_level").takeIf { it > 0 }
+ ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 }
+ ?: 0
+}
+
+@Serializable
+@JvmInline
+value class ReforgeId(val id: String)
+
+fun ItemStack.getReforgeId(): ReforgeId? {
+ return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId)
+}
+
val ItemStack.petData: HypixelPetInfo?
get() = petDataCache(this).getOrNull()
diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt
index 6ddb077..7149379 100644
--- a/src/main/kotlin/util/skyblock/ItemType.kt
+++ b/src/main/kotlin/util/skyblock/ItemType.kt
@@ -13,6 +13,13 @@ value class ItemType private constructor(val name: String) {
return ItemType(name)
}
+ private val obfuscatedRegex = "§[kK].*?(§[0-9a-fA-FrR]|$)".toRegex()
+ fun fromEscapeCodeLore(lore: String): ItemType? {
+ return lore.replace(obfuscatedRegex, "").trim().substringAfter(" ", "")
+ .takeIf { it.isNotEmpty() }
+ ?.let(::ofName)
+ }
+
fun fromItemStack(itemStack: ItemStack): ItemType? {
if (itemStack.petData != null)
return PET
@@ -26,13 +33,31 @@ value class ItemType private constructor(val name: String) {
if (type.isEmpty()) return null
return ofName(type)
}
- return null
+ return itemStack.loreAccordingToNbt.lastOrNull()?.directLiteralStringContent?.let(::fromEscapeCodeLore)
}
+ // TODO: some of those are not actual in game item types, but rather ones included in the repository to splat to multiple in game types. codify those somehow
+
val SWORD = ofName("SWORD")
val DRILL = ofName("DRILL")
val PICKAXE = ofName("PICKAXE")
val GAUNTLET = ofName("GAUNTLET")
+ val LONGSWORD = ofName("LONG SWORD")
+ val EQUIPMENT = ofName("EQUIPMENT")
+ val FISHING_WEAPON = ofName("FISHING WEAPON")
+ val CLOAK = ofName("CLOAK")
+ val BELT = ofName("BELT")
+ val NECKLACE = ofName("NECKLACE")
+ val BRACELET = ofName("BRACELET")
+ val GLOVES = ofName("GLOVES")
+ val ROD = ofName("ROD")
+ val FISHING_ROD = ofName("FISHING ROD")
+ val VACUUM = ofName("VACUUM")
+ val CHESTPLATE = ofName("CHESTPLATE")
+ val LEGGINGS = ofName("LEGGINGS")
+ val HELMET = ofName("HELMET")
+ val BOOTS = ofName("BOOTS")
+ val NIL = ofName("__NIL")
/**
* This one is not really official (it never shows up in game).
diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt
index f26cefe..0244630 100644
--- a/src/main/kotlin/util/skyblock/Rarity.kt
+++ b/src/main/kotlin/util/skyblock/Rarity.kt
@@ -1,5 +1,12 @@
package moe.nea.firmament.util.skyblock
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import moe.nea.firmament.util.StringUtil.words
@@ -10,6 +17,7 @@ import moe.nea.firmament.util.unformattedString
typealias RepoRarity = io.github.moulberry.repo.data.Rarity
+@Serializable(with = Rarity.Serializer::class)
enum class Rarity(vararg altNames: String) {
COMMON,
UNCOMMON,
@@ -24,6 +32,19 @@ enum class Rarity(vararg altNames: String) {
UNKNOWN
;
+ object Serializer : KSerializer<Rarity> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor(Rarity::class.java.name, PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): Rarity {
+ return valueOf(decoder.decodeString().replace(" ", "_"))
+ }
+
+ override fun serialize(encoder: Encoder, value: Rarity) {
+ encoder.encodeString(value.name)
+ }
+ }
+
val names = setOf(name) + altNames
val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name }
diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt
new file mode 100644
index 0000000..c57d457
--- /dev/null
+++ b/src/test/resources/testdata/items/hyperion.snbt
@@ -0,0 +1,96 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ ability_scroll: [
+ "IMPLOSION_SCROLL",
+ "WITHER_SHIELD_SCROLL",
+ "SHADOW_WARP_SCROLL"
+ ],
+ art_of_war_count: 1,
+ champion_combat_xp: 1.3556020889209766E7d,
+ donated_museum: 1b,
+ enchantments: {
+ champion: 10,
+ cleave: 5,
+ critical: 6,
+ cubism: 5,
+ ender_slayer: 6,
+ execute: 5,
+ experience: 3,
+ fire_aspect: 2,
+ first_strike: 4,
+ giant_killer: 6,
+ impaling: 3,
+ lethality: 5,
+ looting: 4,
+ luck: 6,
+ scavenger: 4,
+ smite: 7,
+ syphon: 4,
+ thunderlord: 6,
+ ultimate_wise: 5,
+ vampirism: 5,
+ venomous: 5
+ },
+ hot_potato_count: 15,
+ id: "HYPERION",
+ modifier: "heroic",
+ rarity_upgrades: 1,
+ stats_book: 65934,
+ timestamp: 1658091600000L,
+ upgrade_level: 5,
+ uuid: "a45337aa-9eaa-4e6f-aa27-26a42f8eca95"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Heroic Hyperion "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}',
+ "minecraft:enchantment_glint_override": 1b,
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"1145 "},{"color":"dark_gray","text":"(4271)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+355 "},{"color":"yellow","text":"(+30) "},{"color":"dark_gray","text":"(+1,490.37)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+250 "},{"color":"yellow","text":"(+30) "},{"color":"gold","text":"[+5] "},{"color":"blue","text":"(+50) "},{"color":"dark_gray","text":"(+1,064.55)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+70% "},{"color":"dark_gray","text":"(+317.1%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+7% "},{"color":"blue","text":"(+7%) "},{"color":"dark_gray","text":"(+10.5%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+588 "},{"color":"blue","text":"(+125) "},{"color":"dark_gray","text":"(+2,505.09)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+33 "},{"color":"dark_gray","text":"(+45)"}],"italic":false,"text":""}',
+ '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"✎"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"},{"color":"blue","text":", "},{"color":"blue","text":"Champion X"},{"color":"blue","text":", "},{"color":"blue","text":"Cleave V"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Critical VI"},{"color":"blue","text":", "},{"color":"blue","text":"Cubism V"},{"color":"blue","text":", "},{"color":"blue","text":"Ender Slayer VI"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Execute V"},{"color":"blue","text":", "},{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"Fire Aspect II"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Giant Killer VI"},{"color":"blue","text":", "},{"color":"blue","text":"Impaling III"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Lethality V"},{"color":"blue","text":", "},{"color":"blue","text":"Looting IV"},{"color":"blue","text":", "},{"color":"blue","text":"Luck VI"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Scavenger IV"},{"color":"blue","text":", "},{"color":"blue","text":"Smite VII"},{"color":"blue","text":", "},{"color":"blue","text":"Syphon IV"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Thunderlord VI"},{"color":"blue","text":", "},{"color":"blue","text":"Vampirism V"},{"color":"blue","text":", "},{"color":"blue","text":"Venomous V"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Deals "},{"color":"red","text":"+50% "},{"color":"gray","text":"damage to Withers."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Grants "},{"color":"red","text":"+1 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"green","text":"+2 "},{"color":"aqua","text":"✎"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Intelligence "},{"color":"gray","text":"per "},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"green","text":"Scroll Abilities:"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Wither Impact "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"10 blocks"},{"color":"gray","text":" ahead of you."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Then implode dealing "},{"color":"red","text":"21,658 "},{"color":"gray","text":"damage"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"to nearby enemies. Also applies the"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"wither shield scroll ability reducing"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"damage taken and granting an"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"absorption shield for "},{"color":"yellow","text":"5 "},{"color":"gray","text":"seconds."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"150"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"white","text":"Kills: "},{"color":"gold","text":"65,934"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON SWORD "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}'
+ ],
+ "minecraft:unbreakable": {
+ show_in_tooltip: 0b
+ }
+ },
+ count: 1,
+ id: "minecraft:iron_sword"
+}
diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt
new file mode 100644
index 0000000..b73542d
--- /dev/null
+++ b/src/test/resources/testdata/items/implosion-belt.snbt
@@ -0,0 +1,105 @@
+{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ attributes: {
+ dominance: 1,
+ experience: 1
+ },
+ id: "IMPLOSION_BELT",
+ timestamp: "12/5/22 5:17 PM",
+ uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}',
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}'
+ ],
+ "minecraft:profile": {
+ id: [I;
+ -896440193,
+ -59755884,
+ -1280665573,
+ -1297214643
+ ],
+ properties: [
+ {
+ name: "textures",
+ signature: "",
+ value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9"
+ }
+ ]
+ }
+ },
+ count: 1,
+ id: "minecraft:player_head"
+}{
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ attributes: {
+ dominance: 1,
+ experience: 1
+ },
+ id: "IMPLOSION_BELT",
+ timestamp: "12/5/22 5:17 PM",
+ uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}',
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}'
+ ],
+ "minecraft:profile": {
+ id: [I;
+ -896440193,
+ -59755884,
+ -1280665573,
+ -1297214643
+ ],
+ properties: [
+ {
+ name: "textures",
+ signature: "",
+ value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9"
+ }
+ ]
+ }
+ },
+ count: 1,
+ id: "minecraft:player_head"
+ }