WIP: Reforge recipes

This commit is contained in:
Linnea Gräf
2024-12-24 03:58:43 +01:00
parent fbab19b40f
commit e16c60169b
17 changed files with 858 additions and 93 deletions

View File

@@ -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)
}
@@ -90,6 +92,10 @@ class FirmamentReiPlugin : REIClientPlugin {
registry.registerDisplayGenerator(
SBCraftingRecipe.Category.catIdentifier,
SkyblockCraftingRecipeDynamicGenerator)
registry.registerDisplayGenerator(
SBReforgeRecipe.catIdentifier,
SBReforgeRecipe.DynamicGenerator
)
registry.registerDisplayGenerator(
SBForgeRecipe.Category.categoryIdentifier,
SkyblockForgeRecipeDynamicGenerator)

View File

@@ -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
}
}

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -6,7 +5,7 @@ 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 {
class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
@@ -16,8 +15,7 @@ class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) :
val baseRecipes = repository.items.items.values
.asSequence()
.flatMap { it.recipes }
val extraRecipes = essenceRecipeProvider.recipes
(baseRecipes + extraRecipes)
(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) }

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -7,7 +6,7 @@ import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
class EssenceRecipeProvider : IReloadable {
class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider {
data class EssenceUpgradeRecipe(
val itemId: SkyblockId,
val starCountAfter: Int,
@@ -30,6 +29,8 @@ class EssenceRecipeProvider : IReloadable {
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) {

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.data.NEURecipe
interface ExtraRecipeProvider {
fun provideExtraRecipes(): Iterable<NEURecipe>
}

View File

@@ -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
return this.transformEachRecursively {
var string = it.directLiteralStringContent ?: return@transformEachRecursively it
loreReplacements.forEach { (find, replace) ->
string = string.replace("{$find}", replace)
}
return Text.literal(string).styled { this.style }
Text.literal(string).setStyle(it.style)
}
}
var job: Job? = null

View File

@@ -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>
}
}

View File

@@ -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)
}
}
}

View File

@@ -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() ?: "") }
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -1,5 +1,3 @@
package moe.nea.firmament.util
import net.minecraft.util.Formatting
@@ -28,6 +26,10 @@ enum class LegacyFormattingCode(val label: String, val char: Char, val index: In
ITALIC("ITALIC", 'o', -1),
RESET("RESET", 'r', -1);
companion object {
val byCode = entries.associateBy { it.char }
}
val modern = Formatting.byCode(char)!!
val formattingCode = "§$char"

View File

@@ -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()

View File

@@ -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).

View File

@@ -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 }

View File

@@ -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"
}

View File

@@ -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"
}