diff options
Diffstat (limited to 'src/main/kotlin/repo')
19 files changed, 1153 insertions, 406 deletions
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt index 4b32e57..6d18223 100644 --- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -2,8 +2,10 @@ package moe.nea.firmament.repo import io.github.moulberry.repo.IReloadable import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUNpcShopRecipe import io.github.moulberry.repo.data.NEURecipe import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.skyblockId class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable { var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf() @@ -17,6 +19,9 @@ class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IR .flatMap { it.recipes } (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() }) .forEach { recipe -> + if (recipe is NEUNpcShopRecipe) { + usages.getOrPut(recipe.isSoldBy.skyblockId, ::mutableSetOf).add(recipe) + } recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } } diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt index fbc9eb8..25a74de 100644 --- a/src/main/kotlin/repo/ExpLadder.kt +++ b/src/main/kotlin/repo/ExpLadder.kt @@ -19,7 +19,8 @@ object ExpLadders : IReloadable { val expInCurrentLevel: Float, var expTotal: Float, ) { - val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel } data class ExpLadder( diff --git a/src/main/kotlin/repo/ExpensiveItemCacheApi.kt b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt new file mode 100644 index 0000000..eef95a6 --- /dev/null +++ b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.repo + +/** + * Marker for functions that could potentially invoke DFU. Please do not call on a lot of objects at once, or try to make sure the item is cached and fall back to a more gentle function call using [SBItemStack.isWarm] and similar functions. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +annotation class ExpensiveItemCacheApi diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt index 181aa70..d6be96f 100644 --- a/src/main/kotlin/repo/HypixelStaticData.kt +++ b/src/main/kotlin/repo/HypixelStaticData.kt @@ -1,23 +1,19 @@ package moe.nea.firmament.repo -import io.ktor.client.call.body -import io.ktor.client.request.get import org.apache.logging.log4j.LogManager -import org.lwjgl.glfw.GLFW import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.minutes import moe.nea.firmament.Firmament import moe.nea.firmament.apis.CollectionResponse import moe.nea.firmament.apis.CollectionSkillData -import moe.nea.firmament.keybindings.IKeyBinding import moe.nea.firmament.util.SkyblockId -import moe.nea.firmament.util.async.waitForInput +import moe.nea.firmament.util.net.HttpUtil object HypixelStaticData { private val logger = LogManager.getLogger("Firmament.HypixelStaticData") @@ -25,7 +21,13 @@ object HypixelStaticData { private val hypixelApiBaseUrl = "https://api.hypixel.net" var lowestBin: Map<SkyblockId, Double> = mapOf() private set - var bazaarData: Map<SkyblockId, BazaarData> = mapOf() + var avg1dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var avg3dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var avg7dlowestBin: Map<SkyblockId, Double> = mapOf() + private set + var bazaarData: Map<SkyblockId.BazaarStock, BazaarData> = mapOf() private set var collectionData: Map<String, CollectionSkillData> = mapOf() private set @@ -56,9 +58,11 @@ object HypixelStaticData { val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(), ) - fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item] - fun hasBazaarStock(item: SkyblockId): Boolean { + fun getPriceOfItem(item: SkyblockId): Double? = + bazaarData[SkyblockId.BazaarStock.fromSkyBlockId(item)]?.quickStatus?.buyPrice ?: lowestBin[item] + + fun hasBazaarStock(item: SkyblockId.BazaarStock): Boolean { return item in bazaarData } @@ -74,35 +78,43 @@ object HypixelStaticData { Firmament.coroutineScope.launch { while (true) { logger.info("Updating NEU prices") - updatePrices() - delay(10.minutes) + fetchPricesFromMoulberry() + delay(5.minutes) + } + } + Firmament.coroutineScope.launch { + while (true) { + logger.info("Updating bazaar prices") + fetchBazaarPrices() + delay(2.minutes) } } - } - - private suspend fun updatePrices() { - awaitAll( - Firmament.coroutineScope.async { fetchBazaarPrices() }, - Firmament.coroutineScope.async { fetchPricesFromMoulberry() }, - ) } private suspend fun fetchPricesFromMoulberry() { - lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json") - .body<Map<SkyblockId, Double>>() + lowestBin = HttpUtil.request("$moulberryBaseUrl/lowestbin.json") + .forJson<Map<SkyblockId, Double>>().await() + avg1dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/1day.json") + .forJson<Map<SkyblockId, Double>>().await() + avg3dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/3day.json") + .forJson<Map<SkyblockId, Double>>().await() + avg7dlowestBin = HttpUtil.request("$moulberryBaseUrl/auction_averages_lbin/7day.json") + .forJson<Map<SkyblockId, Double>>().await() } private suspend fun fetchBazaarPrices() { - val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>() + val response = HttpUtil.request("$hypixelApiBaseUrl/skyblock/bazaar").forJson<BazaarResponse>() + .await() if (!response.success) { logger.warn("Retrieved unsuccessful bazaar data") } - bazaarData = response.products.mapKeys { it.key.toRepoId() } + bazaarData = response.products } private suspend fun updateCollectionData() { val response = - Firmament.httpClient.get("$hypixelApiBaseUrl/resources/skyblock/collections").body<CollectionResponse>() + HttpUtil.request("$hypixelApiBaseUrl/resources/skyblock/collections").forJson<CollectionResponse>() + .await() if (!response.success) { logger.warn("Retrieved unsuccessful collection data") } diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index e140dd8..be07042 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -4,70 +4,83 @@ import com.mojang.serialization.Dynamic import io.github.moulberry.repo.IReloadable import io.github.moulberry.repo.NEURepository import io.github.moulberry.repo.data.NEUItem -import io.github.notenoughupdates.moulconfig.xml.Bind import java.text.NumberFormat import java.util.UUID import java.util.concurrent.ConcurrentHashMap import org.apache.logging.log4j.LogManager -import kotlinx.coroutines.Job +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.io.path.readText import kotlin.jvm.optionals.getOrNull import net.minecraft.SharedConstants -import net.minecraft.client.resource.language.I18n -import net.minecraft.component.DataComponentTypes -import net.minecraft.component.type.NbtComponent -import net.minecraft.datafixer.Schemas -import net.minecraft.datafixer.TypeReferences -import net.minecraft.item.ItemStack -import net.minecraft.item.Items -import net.minecraft.nbt.NbtCompound -import net.minecraft.nbt.NbtElement +import net.minecraft.core.component.DataComponents +import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.NbtString -import net.minecraft.text.Style -import net.minecraft.text.Text +import net.minecraft.nbt.StringTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.TagParser +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.MutableComponent +import net.minecraft.network.chat.Style +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.datafix.DataFixers +import net.minecraft.util.datafix.fixes.References +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.item.component.CustomData 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.features.debug.ExportedTestConstantMeta 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.MinecraftDispatcher 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.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loadItemFromNbt 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.skyblockId import moe.nea.firmament.util.transformEachRecursively object ItemCache : IReloadable { private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap() - private val df = Schemas.getFixer() + private val df = DataFixers.getDataFixer() val logger = LogManager.getLogger("${Firmament.logger.name}.ItemCache") var isFlawless = true private set - private fun NEUItem.get10809CompoundTag(): NbtCompound = NbtCompound().apply { + private fun NEUItem.get10809CompoundTag(): CompoundTag = CompoundTag().apply { put("tag", LegacyTagParser.parse(nbttag)) putString("id", minecraftItemId) putByte("Count", 1) putShort("Damage", damage.toShort()) } - private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? = + @ExpensiveItemCacheApi + private fun CompoundTag.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern) + val currentSaveVersion = SharedConstants.getCurrentVersion().dataVersion().version + + @ExpensiveItemCacheApi + fun convert189ToModern(nbtComponent: CompoundTag): CompoundTag? = try { df.update( - TypeReferences.ITEM_STACK, - Dynamic(NbtOps.INSTANCE, this), + References.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, nbtComponent), -1, - SharedConstants.getGameVersion().saveVersion.id - ).value as NbtCompound + currentSaveVersion + ).value as CompoundTag } catch (e: Exception) { isFlawless = false logger.error("Could not data fix up $this", e) @@ -84,24 +97,24 @@ object ItemCache : IReloadable { fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack { return ItemStack(Items.PAINTING).apply { - setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) + setCustomName(Component.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) appendLore( listOf( - Text.stringifiedTranslatable( + Component.translatableEscape( "firmament.repo.brokenitem", (neuItem?.skyblockItemId ?: idHint) ) ) ) - set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(NbtCompound().apply { - put("ID", NbtString.of(neuItem?.skyblockItemId ?: idHint?.neuItem ?: "null")) + set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().apply { + put("ID", StringTag.valueOf(neuItem?.skyblockItemId ?: idHint?.neuItem ?: "null")) })) set(FirmamentDataComponentTypes.IS_BROKEN, true) } } - fun un189Lore(lore: String): Text { - val base = Text.literal("") + fun un189Lore(lore: String): MutableComponent { + val base = Component.literal("") base.setStyle(Style.EMPTY.withItalic(false)) var lastColorCode = Style.EMPTY var readOffset = 0 @@ -112,7 +125,7 @@ object ItemCache : IReloadable { } val text = lore.substring(readOffset, nextCode) if (text.isNotEmpty()) { - base.append(Text.literal(text).setStyle(lastColorCode)) + base.append(Component.literal(text).setStyle(lastColorCode)) } readOffset = nextCode + 2 if (nextCode + 1 < lore.length) { @@ -122,25 +135,55 @@ object ItemCache : IReloadable { if (modernFormatting.isColor) { lastColorCode = Style.EMPTY.withColor(modernFormatting) } else { - lastColorCode = lastColorCode.withFormatting(modernFormatting) + lastColorCode = lastColorCode.applyFormat(modernFormatting) } } } return base } + fun tryFindFromModernFormat(skyblockId: SkyblockId): CompoundTag? { + val overlayFile = + RepoManager.overlayData.getMostModernReadableOverlay(skyblockId, currentSaveVersion) ?: return null + val overlay = TagParser.parseCompoundFully(overlayFile.path.readText()) + val result = ExportedTestConstantMeta.SOURCE_CODEC.decode( + NbtOps.INSTANCE, overlay + ).result().getOrNull() ?: return null + val meta = result.first + return df.update( + References.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, result.second), + meta.dataVersion, + currentSaveVersion + ).value as CompoundTag + } + + @ExpensiveItemCacheApi private fun NEUItem.asItemStackNow(): ItemStack { + try { + var modernItemTag = tryFindFromModernFormat(this.skyblockId) val oldItemTag = get10809CompoundTag() - val modernItemTag = oldItemTag.transformFrom10809ToModern() - ?: return brokenItemStack(this) + var usedOldNbt = false + if (modernItemTag == null) { + usedOldNbt = true + modernItemTag = oldItemTag.transformFrom10809ToModern() + ?: return brokenItemStack(this) + } val itemInstance = - ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this) + loadItemFromNbt( modernItemTag) ?: return brokenItemStack(this) + if (usedOldNbt) { + val tag = oldItemTag.getCompound("tag") + val extraAttributes = tag.flatMap { it.getCompound("ExtraAttributes") } + .getOrNull() + if (extraAttributes != null) + itemInstance.set(DataComponents.CUSTOM_DATA, CustomData.of(extraAttributes)) + val itemModel = tag.flatMap { it.getString("ItemModel") }.getOrNull() + if (itemModel != null) + itemInstance.set(DataComponents.ITEM_MODEL, ResourceLocation.parse(itemModel)) + } itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } itemInstance.displayNameAccordingToNbt = un189Lore(displayName) - val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes") - if (extraAttributes != null) - itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) return itemInstance } catch (e: Exception) { e.printStackTrace() @@ -148,6 +191,11 @@ object ItemCache : IReloadable { } } + fun hasCacheFor(skyblockId: SkyblockId): Boolean { + return skyblockId.neuItem in cache + } + + @ExpensiveItemCacheApi fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map<String, String>? = null): ItemStack { if (this == null) return brokenItemStack(null, idHint) var s = cache[this.skyblockItemId] @@ -158,7 +206,7 @@ object ItemCache : IReloadable { if (!loreReplacements.isNullOrEmpty()) { s = s.copy()!! s.applyLoreReplacements(loreReplacements) - s.setCustomName(s.name.applyLoreReplacements(loreReplacements)) + s.setCustomName(s.hoverName.applyLoreReplacements(loreReplacements)) } return s } @@ -171,67 +219,59 @@ object ItemCache : IReloadable { } } - fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text { + fun Component.applyLoreReplacements(loreReplacements: Map<String, String>): Component { 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) + Component.literal(string).setStyle(it.style) } } - var job: Job? = null - - object ReloadProgressHud : MoulConfigHud( - "repo_reload", HudMeta(HudPosition(0.0, 0.0, 1F), Text.literal("Repo Reload"), 180, 18)) { - - - var isEnabled = false - override fun shouldRender(): Boolean { - return isEnabled - } + var itemRecacheScope: CoroutineScope? = null - @get:Bind("current") - var current: Double = 0.0 + private var recacheSoonSubmitted = mutableSetOf<SkyblockId>() - @get:Bind("label") - var label: String = "" - - @get:Bind("max") - var max: Double = 0.0 - - fun reportProgress(label: String, current: Int, max: Int) { - this.label = label - this.current = current.toDouble() - this.max = max.toDouble() + @OptIn(ExpensiveItemCacheApi::class) + fun recacheSoon(neuItem: NEUItem) { + itemRecacheScope?.launch { + if (!withContext(MinecraftDispatcher) { + recacheSoonSubmitted.add(neuItem.skyblockId) + }) { + return@launch + } + neuItem.asItemStack() } } + @OptIn(ExpensiveItemCacheApi::class) override fun reload(repository: NEURepository) { - val j = job - if (j != null && j.isActive) { - j.cancel() - } + val j = itemRecacheScope + j?.cancel("New reload invoked") cache.clear() isFlawless = true if (TestUtil.isInTest) return - job = Firmament.coroutineScope.launch { - val items = repository.items?.items - if (items == null) { - ReloadProgressHud.isEnabled = false - return@launch - } - val recacheItems = I18n.translate("firmament.repo.cache") - ReloadProgressHud.reportProgress(recacheItems, 0, items.size) - ReloadProgressHud.isEnabled = true - var i = 0 - items.values.forEach { - it.asItemStack() // Rebuild cache - ReloadProgressHud.reportProgress(recacheItems, i++, items.size) - } - ReloadProgressHud.isEnabled = false + val newScope = + CoroutineScope( + Firmament.coroutineScope.coroutineContext + + SupervisorJob(Firmament.globalJob) + + Dispatchers.Default.limitedParallelism( + (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1) + ) + ) + val items = repository.items?.items + newScope.launch { + val items = items ?: return@launch + items.values.chunked(500).map { chunk -> + async { + chunk.forEach { + it.asItemStack() // Rebuild cache + } + } + }.awaitAll() } + itemRecacheScope = newScope } fun coinItem(coinAmount: Int): ItemStack { @@ -249,7 +289,7 @@ object ItemCache : IReloadable { "http://textures.minecraft.net/texture/7b951fed6a7b2cbc2036916dec7a46c4a56481564d14f945b6ebc03382766d3b" } val itemStack = ItemStack(Items.PLAYER_HEAD) - itemStack.setCustomName(Text.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins")) + itemStack.setCustomName(Component.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins")) itemStack.setSkullOwner(uuid, texture) return itemStack } @@ -263,10 +303,10 @@ object ItemCache : IReloadable { } -operator fun NbtCompound.set(key: String, value: String) { +operator fun CompoundTag.set(key: String, value: String) { putString(key, value) } -operator fun NbtCompound.set(key: String, value: NbtElement) { +operator fun CompoundTag.set(key: String, value: Tag) { put(key, value) } diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt new file mode 100644 index 0000000..5b5b016 --- /dev/null +++ b/src/main/kotlin/repo/MiningRepoData.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import java.util.Collections +import java.util.NavigableMap +import java.util.TreeMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.serializer +import kotlin.streams.asSequence +import net.minecraft.world.level.block.Block +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.ReforgeStore.kJson +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loadItemFromNbt +import moe.nea.firmament.util.skyblockId + +class MiningRepoData : IReloadable { + var customMiningAreas: Map<SkyBlockIsland, CustomMiningArea> = mapOf() + private set + var customMiningBlocks: List<CustomMiningBlock> = listOf() + private set + var toolsByBreakingPower: NavigableMap<BreakingPowerKey, SBItemStack> = Collections.emptyNavigableMap() + private set + + + data class BreakingPowerKey( + val breakingPower: Int, + val itemId: SkyblockId? = null + ) { + companion object { + val COMPARATOR: Comparator<BreakingPowerKey> = + Comparator + .comparingInt<BreakingPowerKey> { it.breakingPower } + .thenComparing(Comparator.comparing( + { it.itemId }, + nullsFirst(Comparator.comparing<SkyblockId, Boolean> { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder<SkyblockId>())))) + } + } + + override fun reload(repo: NEURepository) { + customMiningAreas = repo.file("mining/custom_mining_areas.json") + ?.kJson(serializer()) ?: mapOf() + customMiningBlocks = repo.tree("mining/blocks") + .asSequence() + .filter { it.path.endsWith(".json") } + .map { it.kJson(serializer<CustomMiningBlock>()) } + .toList() + toolsByBreakingPower = Collections.unmodifiableNavigableMap( + repo.items.items + .values + .asSequence() + .map { SBItemStack(it.skyblockId) } + .filter { it.breakingPower > 0 } + .associateTo(TreeMap<BreakingPowerKey, SBItemStack>(BreakingPowerKey.COMPARATOR)) { + BreakingPowerKey(it.breakingPower, it.skyblockId) to it + }) + } + + fun getToolsThatCanBreak(breakingPower: Int): Collection<SBItemStack> { + return toolsByBreakingPower.tailMap(BreakingPowerKey(breakingPower, null), true).values + } + + @Serializable + data class CustomMiningBlock( + val breakingPower: Int = 0, + val blockStrength: Int = 0, + val name: String? = null, + val baseDrop: SkyblockId? = null, + val blocks189: List<Block189> = emptyList() + ) { + @Transient + val dropItem = baseDrop?.let(::SBItemStack) + @OptIn(ExpensiveItemCacheApi::class) + private val labeledStack by lazy { + dropItem?.asCopiedItemStack()?.also(::markItemStack) + } + + private fun markItemStack(itemStack: ItemStack) { + itemStack.set(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA, this) + if (name != null) + itemStack.displayNameAccordingToNbt = Component.literal(name) + } + + fun getDisplayItem(block: Block): ItemStack { + return labeledStack ?: ItemStack(block).also(::markItemStack) + } + } + + @Serializable + data class Block189( + val itemId: String, + val damage: Short = 0, + val onlyIn: List<SkyBlockIsland>? = null, + ) { + @Transient + val block = convertToModernBlock() + + val isCurrentlyActive: Boolean + get() = isActiveIn(SBData.skyblockLocation ?: SkyBlockIsland.NIL) + + fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn + + @OptIn(ExpensiveItemCacheApi::class) + private fun convertToModernBlock(): Block? { + // TODO: this should be in a shared util, really + val newCompound = ItemCache.convert189ToModern(CompoundTag().apply { + putString("id", itemId) + putShort("Damage", damage) + }) ?: return null + val itemStack = loadItemFromNbt(newCompound) ?: return null + val blockItem = itemStack.item as? BlockItem ?: return null + return blockItem.block + } + } + + @Serializable + data class CustomMiningArea( + val isSpecialMining: Boolean = true + ) + + +} diff --git a/src/main/kotlin/repo/ModernOverlaysData.kt b/src/main/kotlin/repo/ModernOverlaysData.kt new file mode 100644 index 0000000..543b800 --- /dev/null +++ b/src/main/kotlin/repo/ModernOverlaysData.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import moe.nea.firmament.util.SkyblockId + +// TODO: move this over to the repo parser +class ModernOverlaysData : IReloadable { + data class OverlayFile( + val version: Int, + val path: Path, + ) + + var overlays: Map<SkyblockId, List<OverlayFile>> = mapOf() + override fun reload(repo: NEURepository) { + val items = mutableMapOf<SkyblockId, MutableList<OverlayFile>>() + repo.baseFolder.resolve("itemsOverlay") + .takeIf { it.isDirectory() } + ?.listDirectoryEntries() + ?.forEach { versionFolder -> + val version = versionFolder.fileName.toString().toIntOrNull() ?: return@forEach + versionFolder.listDirectoryEntries() + .forEach { item -> + if (item.extension != "snbt") return@forEach + val itemId = item.nameWithoutExtension + items.getOrPut(SkyblockId(itemId)) { mutableListOf() }.add(OverlayFile(version, item)) + } + } + this.overlays = items + } + + fun getOverlayFiles(skyblockId: SkyblockId) = overlays[skyblockId] ?: listOf() + fun getMostModernReadableOverlay(skyblockId: SkyblockId, version: Int) = getOverlayFiles(skyblockId) + .filter { it.version <= version } + .maxByOrNull { it.version } +} diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt index dc0d93d..5f6506f 100644 --- a/src/main/kotlin/repo/Reforge.kt +++ b/src/main/kotlin/repo/Reforge.kt @@ -13,10 +13,10 @@ 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 net.minecraft.world.item.Item +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.skyblock.ItemType @@ -64,9 +64,9 @@ data class Reforge( } jsonElement["itemId"]?.let { decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach { - val ident = Identifier.tryParse(it) + val ident = ResourceLocation.tryParse(it) if (ident != null) - filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident))) + filters.add(AllowsVanillaItemType(ResourceKey.create(Registries.ITEM, ident))) } } return filters @@ -90,8 +90,8 @@ data class Reforge( return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content)) } jsonObject["minecraftId"]?.let { - return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, - Identifier.of((it as JsonPrimitive).content))) + return AllowsVanillaItemType(ResourceKey.create(Registries.ITEM, + ResourceLocation.parse((it as JsonPrimitive).content))) } error("Unknown item type") } @@ -104,7 +104,7 @@ data class Reforge( data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter - data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter + data class AllowsVanillaItemType(val minecraftId: ResourceKey<Item>) : ReforgeEligibilityFilter } diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt index 4c01974..cf8b434 100644 --- a/src/main/kotlin/repo/ReforgeStore.kt +++ b/src/main/kotlin/repo/ReforgeStore.kt @@ -9,8 +9,8 @@ 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 net.minecraft.world.item.Item +import net.minecraft.resources.ResourceKey import moe.nea.firmament.Firmament import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId @@ -23,7 +23,7 @@ object ReforgeStore : ExtraRecipeProvider, IReloadable { } var byType: Map<ItemType, List<Reforge>> = mapOf() - var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf() + var byVanilla: Map<ResourceKey<Item>, List<Reforge>> = mapOf() var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf() var modifierLut = mapOf<ReforgeId, Reforge>() var byReforgeStone = mapOf<SkyblockId, Reforge>() @@ -52,7 +52,7 @@ object ReforgeStore : ExtraRecipeProvider, IReloadable { byReforgeStone = allReforges.filter { it.reforgeStone != null } .associateBy { it.reforgeStone!! } val byType = mutableMapOf<ItemType, MutableList<Reforge>>() - val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>() + val byVanilla = mutableMapOf<ResourceKey<Item>, MutableList<Reforge>>() val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>() this.byType = byType this.byVanilla = byVanilla diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt index 3efd83b..adbe36e 100644 --- a/src/main/kotlin/repo/RepoDownloadManager.kt +++ b/src/main/kotlin/repo/RepoDownloadManager.kt @@ -1,11 +1,5 @@ - - package moe.nea.firmament.repo -import io.ktor.client.call.body -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsChannel -import io.ktor.utils.io.copyTo import java.io.IOException import java.nio.file.Files import java.nio.file.Path @@ -13,6 +7,7 @@ import java.nio.file.StandardOpenOption import java.util.zip.ZipInputStream import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.future.await import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlin.io.path.createDirectories @@ -23,106 +18,112 @@ import kotlin.io.path.readText import kotlin.io.path.writeText import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament.logger +import moe.nea.firmament.repo.RepoDownloadManager.latestSavedVersionHash import moe.nea.firmament.util.iterate +import moe.nea.firmament.util.net.HttpUtil object RepoDownloadManager { - val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") - val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") - - private fun loadSavedVersionHash(): String? = - if (repoSavedLocation.exists()) { - if (repoMetadataLocation.exists()) { - try { - repoMetadataLocation.readText().trim() - } catch (e: IOException) { - null - } - } else { - null - } - } else null - - private fun saveVersionHash(versionHash: String) { - latestSavedVersionHash = versionHash - repoMetadataLocation.writeText(versionHash) - } - - var latestSavedVersionHash: String? = loadSavedVersionHash() - private set - - @Serializable - private class GithubCommitsResponse(val sha: String) - - private suspend fun requestLatestGithubSha(): String? { - if (RepoManager.Config.branch == "prerelease") { - RepoManager.Config.branch = "master" - } - val response = - Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}") - if (response.status.value != 200) { - return null - } - return response.body<GithubCommitsResponse>().sha - } - - private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { - val response = Firmament.httpClient.get(url) - val targetFile = Files.createTempFile("firmament-repo", ".zip") - val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) - response.bodyAsChannel().copyTo(outputChannel) - targetFile - } - - /** - * Downloads the latest repository from github, setting [latestSavedVersionHash]. - * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) - */ - suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) { - val latestSha = requestLatestGithubSha() - if (latestSha == null) { - logger.warn("Could not request github API to retrieve latest REPO sha.") - return@withContext false - } - val currentSha = loadSavedVersionHash() - if (latestSha != currentSha || force) { - val requestUrl = - "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" - logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") - val zipFile = downloadGithubArchive(requestUrl) - logger.info("Download repository zip file to $zipFile. Deleting old repository") - withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } - logger.info("Extracting new repository") - withContext(IO) { extractNewRepository(zipFile) } - logger.info("Repository loaded on disk.") - saveVersionHash(latestSha) - return@withContext true - } else { - logger.debug("Repository on latest sha $currentSha. Not performing update") - return@withContext false - } - } - - private fun extractNewRepository(zipFile: Path) { - repoSavedLocation.createDirectories() - ZipInputStream(zipFile.inputStream()).use { cis -> - while (true) { - val entry = cis.nextEntry ?: break - if (entry.isDirectory) continue - val extractedLocation = - repoSavedLocation.resolve( - entry.name.substringAfter('/', missingDelimiterValue = "") - ) - if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { - logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") - } - extractedLocation.parent.createDirectories() - extractedLocation.outputStream().use { cis.copyTo(it) } - } - } - } + val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") + val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") + + private fun loadSavedVersionHash(): String? = + if (repoSavedLocation.exists()) { + if (repoMetadataLocation.exists()) { + try { + repoMetadataLocation.readText().trim() + } catch (e: IOException) { + null + } + } else { + null + } + } else null + + private fun saveVersionHash(versionHash: String) { + latestSavedVersionHash = versionHash + repoMetadataLocation.writeText(versionHash) + } + + var latestSavedVersionHash: String? = loadSavedVersionHash() + private set + + @Serializable + private class GithubCommitsResponse(val sha: String) + + private suspend fun requestLatestGithubSha(branchOverride: String?): String? { + if (RepoManager.TConfig.branch == "prerelease") { + RepoManager.TConfig.branch = "master" + } + val response = + HttpUtil.request("https://api.github.com/repos/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/commits/${branchOverride ?: RepoManager.TConfig.branch}") + .forJson<GithubCommitsResponse>() + .await() + return response.sha + } + + private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { + val response = HttpUtil.request(url) + val targetFile = Files.createTempFile("firmament-repo", ".zip") + Files.newOutputStream(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + .use { outputStream -> + response.forInputStream().await().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + targetFile + } + + /** + * Downloads the latest repository from github, setting [latestSavedVersionHash]. + * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) + */ + suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean = + withContext(CoroutineName("Repo Update Check")) { + val latestSha = requestLatestGithubSha(branch) + if (latestSha == null) { + logger.warn("Could not request github API to retrieve latest REPO sha.") + return@withContext false + } + val currentSha = loadSavedVersionHash() + if (latestSha != currentSha || force) { + val requestUrl = + "https://github.com/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/archive/$latestSha.zip" + logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") + val zipFile = downloadGithubArchive(requestUrl) + logger.info("Download repository zip file to $zipFile. Deleting old repository") + withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } + logger.info("Extracting new repository") + withContext(IO) { extractNewRepository(zipFile) } + logger.info("Repository loaded on disk.") + saveVersionHash(latestSha) + return@withContext true + } else { + logger.debug("Repository on latest sha $currentSha. Not performing update") + return@withContext false + } + } + + private fun extractNewRepository(zipFile: Path) { + repoSavedLocation.createDirectories() + ZipInputStream(zipFile.inputStream()).use { cis -> + while (true) { + val entry = cis.nextEntry ?: break + if (entry.isDirectory) continue + val extractedLocation = + repoSavedLocation.resolve( + entry.name.substringAfter('/', missingDelimiterValue = "") + ) + if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { + logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + } + extractedLocation.parent.createDirectories() + extractedLocation.outputStream().use { cis.copyTo(it) } + } + } + } } diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt index 6d9ba14..c8da6a7 100644 --- a/src/main/kotlin/repo/RepoManager.kt +++ b/src/main/kotlin/repo/RepoManager.kt @@ -7,23 +7,28 @@ import io.github.moulberry.repo.data.NEURecipe import io.github.moulberry.repo.data.Rarity import java.nio.file.Path import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import net.minecraft.client.MinecraftClient -import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket -import net.minecraft.recipe.display.CuttingRecipeDisplay +import kotlinx.coroutines.withContext +import net.minecraft.client.Minecraft +import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket +import net.minecraft.world.item.crafting.SelectableRecipe +import net.minecraft.util.StringRepresentable import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament.logger import moe.nea.firmament.events.ReloadRegistrationEvent -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.MC import moe.nea.firmament.util.MinecraftDispatcher import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.TestUtil +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.tr object RepoManager { - object Config : ManagedConfig("repo", Category.META) { + @Config + object TConfig : ManagedConfig("repo", Category.META) { var username by string("username") { "NotEnoughUpdates" } var reponame by string("reponame") { "NotEnoughUpdates-REPO" } var branch by string("branch") { "master" } @@ -32,20 +37,35 @@ object RepoManager { username = "NotEnoughUpdates" reponame = "NotEnoughUpdates-REPO" branch = "master" - save() + markDirty() } - + val enableREI by toggle("enable-rei") { true } val disableItemGroups by toggle("disable-item-groups") { true } val reload by button("reload") { - save() - RepoManager.reload() + markDirty() + Firmament.coroutineScope.launch { + RepoManager.reload() + } } val redownload by button("redownload") { - save() + markDirty() RepoManager.launchAsyncUpdate(true) } val alwaysSuperCraft by toggle("enable-super-craft") { true } var warnForMissingItemListMod by toggle("warn-for-missing-item-list-mod") { true } + val perfectRenders by choice("perfect-renders") { PerfectRender.RENDER } + } + + enum class PerfectRender(val label: String) : StringRepresentable { + NOTHING("nothing"), + RENDER("render"), + RENDER_AND_TEXT("text"), + ; + + fun rendersPerfectText() = this == RENDER_AND_TEXT + fun rendersPerfectVisuals() = this == RENDER || this == RENDER_AND_TEXT + + override fun getSerializedName(): String? = label } val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash @@ -54,15 +74,22 @@ object RepoManager { val essenceRecipeProvider = EssenceRecipeProvider() val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore) + val miningData = MiningRepoData() + val overlayData = ModernOverlaysData() + val enchantedBookCache = EnchantedBookCache() fun makeNEURepository(path: Path): NEURepository { return NEURepository.of(path).apply { + registerReloadListener(overlayData) registerReloadListener(ItemCache) registerReloadListener(RepoItemTypeCache) registerReloadListener(ExpLadders) registerReloadListener(ItemNameLookup) registerReloadListener(ReforgeStore) registerReloadListener(essenceRecipeProvider) + registerReloadListener(recipeCache) + registerReloadListener(miningData) + registerReloadListener(enchantedBookCache) ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this)) registerReloadListener { if (TestUtil.isInTest) return@registerReloadListener @@ -73,7 +100,6 @@ object RepoManager { } } } - registerReloadListener(recipeCache) } } @@ -86,8 +112,8 @@ object RepoManager { fun getUsagesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.usages[skyblockId] ?: setOf() private fun trySendClientboundUpdateRecipesPacket(): Boolean { - return MinecraftClient.getInstance().world != null && MinecraftClient.getInstance().networkHandler?.onSynchronizeRecipes( - SynchronizeRecipesS2CPacket(mutableMapOf(), CuttingRecipeDisplay.Grouping.empty()) + return Minecraft.getInstance().level != null && Minecraft.getInstance().connection?.handleUpdateRecipes( + ClientboundUpdateRecipesPacket(mutableMapOf(), SelectableRecipe.SingleInputSet.empty()) ) != null } @@ -100,47 +126,45 @@ object RepoManager { fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem) + fun downloadOverridenBranch(branch: String) { + Firmament.coroutineScope.launch { + RepoDownloadManager.downloadUpdate(true, branch) + reload() + } + } + fun launchAsyncUpdate(force: Boolean = false) { Firmament.coroutineScope.launch { - ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true - try { - RepoDownloadManager.downloadUpdate(force) - ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1) - } finally { - ItemCache.ReloadProgressHud.isEnabled = false - } + RepoDownloadManager.downloadUpdate(force) reload() } } fun reloadForTest(from: Path) { neuRepo = makeNEURepository(from) - reload() + reloadSync() } - fun reload() { - if (!TestUtil.isInTest && !MC.instance.isOnThread) { - MC.instance.send { - reload() - } - return + + suspend fun reload() { + withContext(Dispatchers.IO) { + reloadSync() } + } + + fun reloadSync() { try { - ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk", - 0, - -1) // TODO: replace with a proper bouncy bar - ItemCache.ReloadProgressHud.isEnabled = true logger.info("Repo reload started.") neuRepo.reload() logger.info("Repo reload completed.") } catch (exc: NEURepositoryException) { ErrorUtil.softError("Failed to reload repository", exc) MC.sendChat( - tr("firmament.repo.reloadfail", - "Failed to reload repository. This will result in some mod features not working.") + tr( + "firmament.repo.reloadfail", + "Failed to reload repository. This will result in some mod features not working." + ) ) - ItemCache.ReloadProgressHud.isEnabled = false } } @@ -153,10 +177,12 @@ object RepoManager { return } neuRepo = makeNEURepository(RepoDownloadManager.repoSavedLocation) - if (Config.autoUpdate) { + if (TConfig.autoUpdate) { launchAsyncUpdate() } else { - reload() + Firmament.coroutineScope.launch { + reload() + } } } @@ -182,6 +208,8 @@ object RepoManager { } fun getRepoRef(): String { - return "${Config.username}/${Config.reponame}#${Config.branch}" + return "${TConfig.username}/${TConfig.reponame}#${TConfig.branch}" } + + fun shouldLoadREI(): Boolean = TConfig.enableREI } diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt index 617efec..cd0bc63 100644 --- a/src/main/kotlin/repo/RepoModResourcePack.kt +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -3,49 +3,50 @@ package moe.nea.firmament.repo import java.io.InputStream import java.nio.file.Files import java.nio.file.Path -import java.util.* +import java.util.Optional import net.fabricmc.fabric.api.resource.ModResourcePack +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.metadata.ModMetadata import kotlin.io.path.exists import kotlin.io.path.isRegularFile import kotlin.io.path.relativeTo import kotlin.streams.asSequence -import net.minecraft.resource.AbstractFileResourcePack -import net.minecraft.resource.InputSupplier -import net.minecraft.resource.NamespaceResourceManager -import net.minecraft.resource.Resource -import net.minecraft.resource.ResourcePack -import net.minecraft.resource.ResourcePackInfo -import net.minecraft.resource.ResourcePackSource -import net.minecraft.resource.ResourceType -import net.minecraft.resource.metadata.ResourceMetadata -import net.minecraft.resource.metadata.ResourceMetadataSerializer -import net.minecraft.text.Text -import net.minecraft.util.Identifier -import net.minecraft.util.PathUtil +import net.minecraft.server.packs.AbstractPackResources +import net.minecraft.server.packs.resources.IoSupplier +import net.minecraft.server.packs.resources.FallbackResourceManager +import net.minecraft.server.packs.resources.Resource +import net.minecraft.server.packs.PackResources +import net.minecraft.server.packs.PackLocationInfo +import net.minecraft.server.packs.repository.PackSource +import net.minecraft.server.packs.PackType +import net.minecraft.server.packs.resources.ResourceMetadata +import net.minecraft.server.packs.metadata.MetadataSectionType +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.FileUtil import moe.nea.firmament.Firmament class RepoModResourcePack(val basePath: Path) : ModResourcePack { companion object { - fun append(packs: MutableList<in ModResourcePack>) { + fun append(packs: ModResourcePackSorter) { Firmament.logger.info("Registering mod resource pack") - packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) } - fun createResourceDirectly(identifier: Identifier): Optional<Resource> { + fun createResourceDirectly(identifier: ResourceLocation): Optional<Resource> { val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation) return Optional.of( Resource( pack, - pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() + pack.getResource(PackType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() ) { val base = - pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) + pack.getResource(PackType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) if (base == null) - ResourceMetadata.NONE + ResourceMetadata.EMPTY else - NamespaceResourceManager.loadMetadata(base) + FallbackResourceManager.parseMetadata(base) } ) } @@ -54,32 +55,32 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { override fun close() { } - override fun openRoot(vararg segments: String): InputSupplier<InputStream>? { - return getFile(segments)?.let { InputSupplier.create(it) } + override fun getRootResource(vararg segments: String): IoSupplier<InputStream>? { + return getFile(segments)?.let { IoSupplier.create(it) } } fun getFile(segments: Array<out String>): Path? { - PathUtil.validatePath(*segments) + FileUtil.validatePath(*segments) val path = segments.fold(basePath, Path::resolve) if (!path.isRegularFile()) return null return path } - override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? { - if (type != ResourceType.CLIENT_RESOURCES) return null + override fun getResource(type: PackType?, id: ResourceLocation): IoSupplier<InputStream>? { + if (type != PackType.CLIENT_RESOURCES) return null if (id.namespace != "neurepo") return null val file = getFile(id.path.split("/").toTypedArray()) - return file?.let { InputSupplier.create(it) } + return file?.let { IoSupplier.create(it) } } - override fun findResources( - type: ResourceType?, + override fun listResources( + type: PackType?, namespace: String, prefix: String, - consumer: ResourcePack.ResultConsumer + consumer: PackResources.ResourceOutput ) { if (namespace != "neurepo") return - if (type != ResourceType.CLIENT_RESOURCES) return + if (type != PackType.CLIENT_RESOURCES) return val prefixPath = basePath.resolve(prefix) if (!prefixPath.exists()) @@ -88,17 +89,20 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { .asSequence() .map { it.relativeTo(basePath) } .forEach { - consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it)) + consumer.accept( + ResourceLocation.tryBuild("neurepo", it.toString()) ?: return@forEach, + IoSupplier.create(it) + ) } } - override fun getNamespaces(type: ResourceType?): Set<String> { - if (type != ResourceType.CLIENT_RESOURCES) return emptySet() + override fun getNamespaces(type: PackType?): Set<String> { + if (type != PackType.CLIENT_RESOURCES) return emptySet() return setOf("neurepo") } - override fun <T : Any?> parseMetadata(metadataSerializer: ResourceMetadataSerializer<T>?): T? { - return AbstractFileResourcePack.parseMetadata( + override fun <T : Any?> getMetadataSection(metadataSerializer: MetadataSectionType<T>?): T? { + return AbstractPackResources.getMetadataFromStream( metadataSerializer, """ { "pack": { @@ -106,13 +110,13 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack { "description": "NEU Repo Resources" } } -""".trimIndent().byteInputStream() +""".trimIndent().byteInputStream(), location() ) } - override fun getInfo(): ResourcePackInfo { - return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty()) + override fun location(): PackLocationInfo { + return PackLocationInfo("neurepo", Component.literal("NEU Repo"), PackSource.BUILT_IN, Optional.empty()) } override fun getFabricModMetadata(): ModMetadata { diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index 4d07801..89e53e8 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -5,20 +5,22 @@ import com.mojang.serialization.codecs.RecordCodecBuilder import io.github.moulberry.repo.constants.PetNumbers import io.github.moulberry.repo.data.NEUIngredient import io.github.moulberry.repo.data.NEUItem -import net.minecraft.item.ItemStack -import net.minecraft.network.RegistryByteBuf -import net.minecraft.network.codec.PacketCodec -import net.minecraft.network.codec.PacketCodecs -import net.minecraft.text.Style -import net.minecraft.text.Text -import net.minecraft.text.TextColor -import net.minecraft.util.Formatting +import net.minecraft.world.item.ItemStack +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.TextColor +import net.minecraft.ChatFormatting import moe.nea.firmament.repo.ItemCache.asItemStack import moe.nea.firmament.repo.ItemCache.withFallback import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.MC import moe.nea.firmament.util.ReforgeId import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.blue import moe.nea.firmament.util.directLiteralStringContent import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.getReforgeId @@ -27,12 +29,17 @@ import moe.nea.firmament.util.grey 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.mc.modifyLore +import moe.nea.firmament.util.modifyExtraAttributes import moe.nea.firmament.util.petData import moe.nea.firmament.util.prepend +import moe.nea.firmament.util.reconstitute +import moe.nea.firmament.util.removeColorCodes import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.skyblock.Rarity import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch import moe.nea.firmament.util.withColor @@ -41,7 +48,7 @@ data class SBItemStack constructor( val neuItem: NEUItem?, private var stackSize: Int, private var petData: PetData?, - val extraLore: List<Text> = emptyList(), + val extraLore: List<Component> = emptyList(), val stars: Int = 0, val fallback: ItemStack? = null, val reforge: ReforgeId? = null, @@ -60,9 +67,9 @@ data class SBItemStack constructor( } companion object { - val PACKET_CODEC: PacketCodec<in RegistryByteBuf, SBItemStack> = PacketCodec.tuple( + val PACKET_CODEC: StreamCodec<in RegistryFriendlyByteBuf, SBItemStack> = StreamCodec.composite( SkyblockId.PACKET_CODEC, { it.skyblockId }, - PacketCodecs.VAR_INT, { it.stackSize }, + ByteBufCodecs.VAR_INT, { it.stackSize }, { id, count -> SBItemStack(id, count) } ) val CODEC: Codec<SBItemStack> = RecordCodecBuilder.create { @@ -75,6 +82,7 @@ data class SBItemStack constructor( } val EMPTY = SBItemStack(SkyblockId.NULL, 0) + private val BREAKING_POWER_REGEX = "Breaking Power (?<power>[0-9]+)".toPattern() operator fun invoke(itemStack: ItemStack): SBItemStack { val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL return SBItemStack( @@ -99,6 +107,13 @@ data class SBItemStack constructor( return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack) } + fun parseStatBlock(itemStack: ItemStack): List<StatLine> { + return itemStack.loreAccordingToNbt + .map { parseStatLine(it) } + .takeWhile { it != null } + .filterNotNull() + } + fun appendEnhancedStats( itemStack: ItemStack, reforgeStats: Map<String, Double>, @@ -121,7 +136,7 @@ data class SBItemStack constructor( loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute() } if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) { - loreMut.add(0, Text.literal("")) + loreMut.add(0, Component.literal("")) } // If there is no stat block the statBlockLastIndex falls through to -1 // TODO: this is good enough for some items. some other items might have their stats at a different place. @@ -134,46 +149,47 @@ data class SBItemStack constructor( data class StatFormatting( val postFix: String, - val color: Formatting, + val color: ChatFormatting, + val isStarAffected: Boolean = true, ) val formattingOverrides = mapOf( - "Sea Creature Chance" to StatFormatting("%", Formatting.RED), - "Strength" to StatFormatting("", Formatting.RED), - "Damage" to StatFormatting("", Formatting.RED), - "Bonus Attack Speed" to StatFormatting("%", Formatting.RED), - "Shot Cooldown" to StatFormatting("s", Formatting.RED), - "Ability Damage" to StatFormatting("%", Formatting.RED), - "Crit Damage" to StatFormatting("%", Formatting.RED), - "Crit Chance" to StatFormatting("%", Formatting.RED), - "Ability Damage" to StatFormatting("%", Formatting.RED), - "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN), - "Health" to StatFormatting("", Formatting.GREEN), - "Defense" to StatFormatting("", Formatting.GREEN), - "Fishing Speed" to StatFormatting("", Formatting.GREEN), - "Double Hook Chance" to StatFormatting("%", Formatting.GREEN), - "Mining Speed" to StatFormatting("", Formatting.GREEN), - "Mining Fortune" to StatFormatting("", Formatting.GREEN), - "Heat Resistance" to StatFormatting("", Formatting.GREEN), - "Swing Range" to StatFormatting("", Formatting.GREEN), - "Rift Time" to StatFormatting("", Formatting.GREEN), - "Speed" to StatFormatting("", Formatting.GREEN), - "Farming Fortune" to StatFormatting("", Formatting.GREEN), - "True Defense" to StatFormatting("", Formatting.GREEN), - "Mending" to StatFormatting("", Formatting.GREEN), - "Foraging Wisdom" to StatFormatting("", Formatting.GREEN), - "Farming Wisdom" to StatFormatting("", Formatting.GREEN), - "Foraging Fortune" to StatFormatting("", Formatting.GREEN), - "Magic Find" to StatFormatting("", Formatting.GREEN), - "Ferocity" to StatFormatting("", Formatting.GREEN), - "Bonus Pest Chance" to StatFormatting("%", Formatting.GREEN), - "Cold Resistance" to StatFormatting("", Formatting.GREEN), - "Pet Luck" to StatFormatting("", Formatting.GREEN), - "Fear" to StatFormatting("", Formatting.GREEN), - "Mana Regen" to StatFormatting("%", Formatting.GREEN), - "Rift Damage" to StatFormatting("", Formatting.GREEN), - "Hearts" to StatFormatting("", Formatting.GREEN), - "Vitality" to StatFormatting("", Formatting.GREEN), + "Sea Creature Chance" to StatFormatting("%", ChatFormatting.RED), + "Strength" to StatFormatting("", ChatFormatting.RED), + "Damage" to StatFormatting("", ChatFormatting.RED), + "Bonus Attack Speed" to StatFormatting("%", ChatFormatting.RED), + "Shot Cooldown" to StatFormatting("s", ChatFormatting.GREEN, false), + "Ability Damage" to StatFormatting("%", ChatFormatting.RED), + "Crit Damage" to StatFormatting("%", ChatFormatting.RED), + "Crit Chance" to StatFormatting("%", ChatFormatting.RED), + "Ability Damage" to StatFormatting("%", ChatFormatting.RED), + "Trophy Fish Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Health" to StatFormatting("", ChatFormatting.GREEN), + "Defense" to StatFormatting("", ChatFormatting.GREEN), + "Fishing Speed" to StatFormatting("", ChatFormatting.GREEN), + "Double Hook Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Mining Speed" to StatFormatting("", ChatFormatting.GREEN), + "Mining Fortune" to StatFormatting("", ChatFormatting.GREEN), + "Heat Resistance" to StatFormatting("", ChatFormatting.GREEN), + "Swing Range" to StatFormatting("", ChatFormatting.GREEN), + "Rift Time" to StatFormatting("", ChatFormatting.GREEN), + "Speed" to StatFormatting("", ChatFormatting.GREEN), + "Farming Fortune" to StatFormatting("", ChatFormatting.GREEN), + "True Defense" to StatFormatting("", ChatFormatting.GREEN), + "Mending" to StatFormatting("", ChatFormatting.GREEN), + "Foraging Wisdom" to StatFormatting("", ChatFormatting.GREEN), + "Farming Wisdom" to StatFormatting("", ChatFormatting.GREEN), + "Foraging Fortune" to StatFormatting("", ChatFormatting.GREEN), + "Magic Find" to StatFormatting("", ChatFormatting.GREEN), + "Ferocity" to StatFormatting("", ChatFormatting.GREEN), + "Bonus Pest Chance" to StatFormatting("%", ChatFormatting.GREEN), + "Cold Resistance" to StatFormatting("", ChatFormatting.GREEN), + "Pet Luck" to StatFormatting("", ChatFormatting.GREEN), + "Fear" to StatFormatting("", ChatFormatting.GREEN), + "Mana Regen" to StatFormatting("%", ChatFormatting.GREEN), + "Rift Damage" to StatFormatting("", ChatFormatting.GREEN), + "Hearts" to StatFormatting("", ChatFormatting.GREEN), + "Vitality" to StatFormatting("", ChatFormatting.GREEN), // TODO: make this a repo json ) @@ -181,20 +197,22 @@ data class SBItemStack constructor( private val statLabelRegex = "(?<statName>.*): ".toPattern() enum class BuffKind( - val color: Formatting, + val color: ChatFormatting, val prefix: String, val postFix: String, + val isHidden: Boolean, ) { - REFORGE(Formatting.BLUE, "(", ")"), - + REFORGE(ChatFormatting.BLUE, "(", ")", false), + STAR_BUFF(ChatFormatting.RESET, "", "", true), + CATA_STAR_BUFF(ChatFormatting.DARK_GRAY, "(", ")", false), ; } data class StatLine( val statName: String, - val value: Text?, - val rest: List<Text> = listOf(), - val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull() + val value: Component?, + val rest: List<Component> = listOf(), + val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.toDoubleOrNull() ) { fun addStat(amount: Double, buffKind: BuffKind): StatLine { val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true) @@ -202,21 +220,29 @@ data class SBItemStack constructor( valueNum = (valueNum ?: 0.0) + amount, value = null, rest = rest + - listOf( - Text.literal( + if (buffKind.isHidden) emptyList() + else listOf( + Component.literal( buffKind.prefix + formattedAmount + statFormatting.postFix + - buffKind.postFix + " ") - .withColor(buffKind.color))) + buffKind.postFix + " " + ) + .withColor(buffKind.color) + ) + ) } fun formatValue() = - Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0, - 1, - includeSign = true) + statFormatting.postFix + " ") + Component.literal( + FirmFormatters.formatCommas( + valueNum ?: 0.0, + 1, + includeSign = true + ) + statFormatting.postFix + " " + ) .setStyle(Style.EMPTY.withColor(statFormatting.color)) - val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN) + val statFormatting = formattingOverrides[statName] ?: StatFormatting("", ChatFormatting.GREEN) private fun abbreviate(abbreviateTo: Int): String { if (abbreviateTo >= statName.length) return statName val segments = statName.split(" ") @@ -225,9 +251,9 @@ data class SBItemStack constructor( } } - fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text = - Text.literal("").setStyle(Style.EMPTY.withItalic(false)) - .append(Text.literal("${abbreviate(abbreviateTo)}: ").grey()) + fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Component = + Component.literal("").setStyle(Style.EMPTY.withItalic(false)) + .append(Component.literal("${abbreviate(abbreviateTo)}: ").grey()) .append(value ?: formatValue()) .also { rest.forEach(it::append) } } @@ -237,10 +263,10 @@ data class SBItemStack constructor( return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } } } - private fun parseStatLine(line: Text): StatLine? { + fun parseStatLine(line: Component): StatLine? { val sibs = line.siblings val stat = sibs.firstOrNull() ?: return null - if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null + if (stat.style.color != TextColor.fromLegacyFormat(ChatFormatting.GRAY)) return null val statLabel = stat.directLiteralStringContent ?: return null val statName = statLabelRegex.useMatch(statLabel) { group("statName") } ?: return null return StatLine(statName, sibs[1], sibs.subList(2, sibs.size)) @@ -303,19 +329,47 @@ data class SBItemStack constructor( val reforge = ReforgeStore.modifierLut[reforgeId] ?: return val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf() itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() - .prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE)) + .prepend( + Component.literal(reforge.reforgeName + " ").withStyle(Rarity.colourMap[rarity] ?: ChatFormatting.WHITE) + ) val data = itemStack.extraAttributes.copy() data.putString("modifier", reforgeId.id) itemStack.extraAttributes = data appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE) + reforge.reforgeAbility?.get(rarity)?.let { reforgeAbility -> + val formattedReforgeAbility = ItemCache.un189Lore(reforgeAbility) + .grey() + itemStack.modifyLore { + val lastBlank = it.indexOfLast { it.unformattedString.isBlank() } + val newList = mutableListOf<Component>() + newList.addAll(it.subList(0, lastBlank)) + newList.add(Component.literal("")) + newList.add(Component.literal("${reforge.reforgeName} Bonus").blue()) + MC.font.splitter.splitLines(formattedReforgeAbility, 180, Style.EMPTY).mapTo(newList) { + it.reconstitute() + } + newList.addAll(it.subList(lastBlank, it.size)) + return@modifyLore newList + } + } } // TODO: avoid instantiating the item stack here + @ExpensiveItemCacheApi val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack()) + + @ExpensiveItemCacheApi val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack()) private var itemStack_: ItemStack? = null + val breakingPower: Int + get() = + BREAKING_POWER_REGEX.useMatch(neuItem?.lore?.firstOrNull()?.removeColorCodes()) { + group("power").toInt() + } ?: 0 + + @ExpensiveItemCacheApi private val itemStack: ItemStack get() { val itemStack = itemStack_ ?: run { @@ -325,12 +379,14 @@ data class SBItemStack constructor( return@run ItemStack.EMPTY val replacementData = mutableMapOf<String, String>() injectReplacementDataForPets(replacementData) - return@run neuItem.asItemStack(idHint = skyblockId, replacementData) + val baseItem = neuItem.asItemStack(idHint = skyblockId, replacementData) .withFallback(fallback) .copyWithCount(stackSize) - .also { appendReforgeInfo(it) } - .also { it.appendLore(extraLore) } - .also { enhanceStatsByStars(it, stars) } + val baseStats = parseStatBlock(baseItem) + appendReforgeInfo(baseItem) + baseItem.appendLore(extraLore) + enhanceStatsByStars(baseItem, stars, baseStats) + return@run baseItem } if (itemStack_ == null) itemStack_ = itemStack @@ -338,38 +394,74 @@ data class SBItemStack constructor( } - private fun starString(stars: Int): Text { - if (stars <= 0) return Text.empty() + /** + * estimate the lore content without creating an itemstack instance + */ + fun estimateLore(): List<Component> { + return (neuItem?.lore?.map { ItemCache.un189Lore(it) } ?: emptyList()) + extraLore + } + + private fun starString(stars: Int): Component { + if (stars <= 0) return Component.empty() + // TODO: idk master stars val tiers = listOf( LegacyFormattingCode.GOLD, LegacyFormattingCode.LIGHT_PURPLE, LegacyFormattingCode.AQUA, ) val maxStars = 5 - if (stars > tiers.size * maxStars) return Text.literal(" ${stars}✪").withColor(Formatting.RED) + if (stars > tiers.size * maxStars) return Component.literal(" ${stars}✪").withColor(ChatFormatting.RED) val starBaseTier = (stars - 1) / maxStars val starBaseColor = tiers[starBaseTier] val starsInCurrentTier = stars - starBaseTier * maxStars - val starString = Text.literal(" " + "✪".repeat(starsInCurrentTier)).withColor(starBaseColor.modern) + val starString = Component.literal(" " + "✪".repeat(starsInCurrentTier)).withColor(starBaseColor.modern) if (starBaseTier > 0) { val starLastTier = tiers[starBaseTier - 1] val starsInLastTier = 5 - starsInCurrentTier - starString.append(Text.literal("✪".repeat(starsInLastTier)).withColor(starLastTier.modern)) + starString.append(Component.literal("✪".repeat(starsInLastTier)).withColor(starLastTier.modern)) } return starString } - private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int) { + private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int, baseStats: List<StatLine>) { if (stars == 0) return // TODO: increase stats and add the star level into the nbt data so star displays work + itemStack.modifyExtraAttributes { + it.putInt("upgrade_level", stars) + } itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() .append(starString(stars)) + val isDungeon = ItemType.fromItemStack(itemStack)?.isDungeon ?: true + val truncatedStarCount = if (isDungeon) minOf(5, stars) else stars + appendEnhancedStats( + itemStack, + baseStats + .filter { it.statFormatting.isStarAffected } + .associate { + it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02)) + }, + BuffKind.STAR_BUFF + ) + } + + fun isWarm(): Boolean { + if (itemStack_ != null) return true + if (ItemCache.hasCacheFor(skyblockId)) return true + return false + } + + @OptIn(ExpensiveItemCacheApi::class) + fun asLazyImmutableItemStack(): ItemStack? { + if (isWarm()) return asImmutableItemStack() + return null } - fun asImmutableItemStack(): ItemStack { + @ExpensiveItemCacheApi + fun asImmutableItemStack(): ItemStack { // TODO: add a "or fallback to painting" option to asLazyImmutableItemStack to be used in more places. return itemStack } + @ExpensiveItemCacheApi fun asCopiedItemStack(): ItemStack { return itemStack.copy() } diff --git a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt index 9a1aea5..84f1f48 100644 --- a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt @@ -3,17 +3,21 @@ package moe.nea.firmament.repo.recipes import io.github.moulberry.repo.NEURepository import io.github.moulberry.repo.data.NEURecipe import me.shedaniel.math.Rectangle -import net.minecraft.item.ItemStack -import net.minecraft.text.Text -import net.minecraft.util.Identifier +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.repo.SBItemStack -interface GenericRecipeRenderer<T : NEURecipe> { - fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter) +interface GenericRecipeRenderer<T : Any> { + fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?) fun getInputs(recipe: T): Collection<SBItemStack> fun getOutputs(recipe: T): Collection<SBItemStack> val icon: ItemStack - val title: Text - val identifier: Identifier + val title: Component + val identifier: ResourceLocation fun findAllRecipes(neuRepository: NEURepository): Iterable<T> + fun discoverExtraRecipes(neuRepository: NEURepository, itemStack: SBItemStack, mustBeInOutputs: Boolean): Iterable<T> = emptyList() + val displayHeight: Int get() = 66 + val displayWidth: Int get() = 150 + val typ: Class<T> } diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt index 109bff5..7a63941 100644 --- a/src/main/kotlin/repo/recipes/RecipeLayouter.kt +++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt @@ -1,7 +1,12 @@ package moe.nea.firmament.repo.recipes import io.github.notenoughupdates.moulconfig.gui.GuiComponent -import net.minecraft.text.Text +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.npc.Villager import moe.nea.firmament.repo.SBItemStack interface RecipeLayouter { @@ -13,21 +18,49 @@ interface RecipeLayouter { * Create a bigger background and mark the slot as output. The coordinates should still refer the upper left corner of the item stack, not of the bigger background. */ BIG_OUTPUT, + DISPLAY,; + val isBig get() = this == BIG_OUTPUT } + + fun createCyclingItemSlot( + x: Int, y: Int, + content: List<SBItemStack>, + slotKind: SlotKind + ): CyclingItemSlot + fun createItemSlot( x: Int, y: Int, content: SBItemStack?, slotKind: SlotKind, - ) + ): ItemSlot = createCyclingItemSlot(x, y, listOfNotNull(content), slotKind) + + interface CyclingItemSlot : ItemSlot { + fun onUpdate(action: () -> Unit) + } + + interface ItemSlot : Updater<SBItemStack> { + fun current(): SBItemStack + } + + interface Updater<T> { + fun update(newValue: T) + } + + fun createTooltip(rectangle: Rectangle, label: List<Component>) + fun createTooltip(rectangle: Rectangle, vararg label: Component) = + createTooltip(rectangle, label.toList()) + fun createLabel( x: Int, y: Int, - text: Text - ) + text: Component + ): Updater<Component> - fun createArrow(x: Int, y: Int) + fun createArrow(x: Int, y: Int): Rectangle fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent) + fun createFire(point: Point, animationTicks: Int) + fun createEntity(rectangle: Rectangle, entity: LivingEntity) } diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt index 679aec8..0a0d5e2 100644 --- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt +++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt @@ -4,25 +4,40 @@ import io.github.moulberry.repo.NEURepository import io.github.moulberry.repo.data.NEUCraftingRecipe import me.shedaniel.math.Point import me.shedaniel.math.Rectangle -import net.minecraft.block.Blocks -import net.minecraft.item.ItemStack -import net.minecraft.text.Text -import net.minecraft.util.Identifier +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.repo.SBItemStack import moe.nea.firmament.util.tr -class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { - override fun render(recipe: NEUCraftingRecipe, bounds: Rectangle, layouter: RecipeLayouter) { +object SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { + override fun render( + recipe: NEUCraftingRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { val point = Point(bounds.centerX - 58, bounds.centerY - 27) - layouter.createArrow(point.x + 60, point.y + 18) + val arrow = layouter.createArrow(point.x + 60, point.y + 18) + + if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) { + layouter.createTooltip( + arrow, + Component.nullToEmpty(recipe.extraText!!), + ) + } + for (i in 0 until 3) { for (j in 0 until 3) { val item = recipe.inputs[i + j * 3] - layouter.createItemSlot(point.x + 1 + i * 18, - point.y + 1 + j * 18, - SBItemStack(item), - RecipeLayouter.SlotKind.SMALL_INPUT) + layouter.createItemSlot( + point.x + 1 + i * 18, + point.y + 1 + j * 18, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT + ) } } layouter.createItemSlot( @@ -32,6 +47,9 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { ) } + override val typ: Class<NEUCraftingRecipe> + get() = NEUCraftingRecipe::class.java + override fun getInputs(recipe: NEUCraftingRecipe): Collection<SBItemStack> { return recipe.allInputs.mapNotNull { SBItemStack(it) } } @@ -45,6 +63,6 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> { } override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE) - override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") // TODO: fix tr not being included in jars - override val identifier: Identifier = Firmament.identifier("crafting_recipe") + override val title: Component = tr("firmament.category.crafting", "SkyBlock Crafting") + override val identifier: ResourceLocation = Firmament.identifier("crafting_recipe") } diff --git a/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt new file mode 100644 index 0000000..15785bd --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt @@ -0,0 +1,74 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import me.shedaniel.math.Rectangle +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.EssenceRecipeProvider +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.tr + +object SBEssenceUpgradeRecipeRenderer : GenericRecipeRenderer<EssenceRecipeProvider.EssenceUpgradeRecipe> { + override fun render( + recipe: EssenceRecipeProvider.EssenceUpgradeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack? + ) { + val sourceItem = mainItem ?: SBItemStack(recipe.itemId) + layouter.createItemSlot( + bounds.minX + 12, + bounds.centerY - 8 - 18 / 2, + sourceItem.copy(stars = recipe.starCountAfter - 1), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.minX + 12, bounds.centerY - 8 + 18 / 2, + SBItemStack(recipe.essenceIngredient), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.maxX - 12 - 16, bounds.centerY - 8, + sourceItem.copy(stars = recipe.starCountAfter), + RecipeLayouter.SlotKind.SMALL_OUTPUT + ) + val extraItems = recipe.extraItems + layouter.createArrow( + bounds.centerX - 24 / 2, + if (extraItems.isEmpty()) bounds.centerY - 17 / 2 + else bounds.centerY + 18 / 2 + ) + for ((index, item) in extraItems.withIndex()) { + layouter.createItemSlot( + bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18, + bounds.centerY - 18 / 2, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + + override fun getInputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> { + return recipe.allInputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.itemId)) + } + + @OptIn(ExpensiveItemCacheApi::class) + override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack() + override val title: Component = tr("firmament.category.essence", "Essence Upgrades") + override val identifier: ResourceLocation = Firmament.identifier("essence_upgrade") + override fun findAllRecipes(neuRepository: NEURepository): Iterable<EssenceRecipeProvider.EssenceUpgradeRecipe> { + return RepoManager.essenceRecipeProvider.recipes + } + + override val typ: Class<EssenceRecipeProvider.EssenceUpgradeRecipe> + get() = EssenceRecipeProvider.EssenceUpgradeRecipe::class.java +} diff --git a/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt new file mode 100644 index 0000000..b595f07 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt @@ -0,0 +1,88 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUForgeRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import kotlin.math.cos +import kotlin.math.sin +import kotlin.time.Duration.Companion.seconds +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.item.ItemStack +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.tr + +object SBForgeRecipeRenderer : GenericRecipeRenderer<NEUForgeRecipe> { + override fun render( + recipe: NEUForgeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { + val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2) + val tooltip = Component.empty() + .append(Component.translatableEscape( + "firmament.recipe.forge.time", + recipe.duration.seconds, + )) + + if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) { + tooltip + .append(Component.nullToEmpty("\n")) + .append(Component.nullToEmpty(recipe.extraText)) + } + + layouter.createTooltip(arrow, tooltip) + + val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) + layouter.createFire(ingredientsCenter, 25) + val count = recipe.inputs.size + if (count == 1) { + layouter.createItemSlot( + ingredientsCenter.x, ingredientsCenter.y - 18, + SBItemStack(recipe.inputs.single()), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } else { + recipe.inputs.forEachIndexed { idx, ingredient -> + val rad = Math.PI * 2 * idx / count + layouter.createItemSlot( + (ingredientsCenter.x + cos(rad) * 30).toInt(), (ingredientsCenter.y + sin(rad) * 30).toInt(), + SBItemStack(ingredient), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + layouter.createItemSlot( + bounds.minX + 124, bounds.minY + 46, + SBItemStack(recipe.outputStack), + RecipeLayouter.SlotKind.BIG_OUTPUT + ) + } + + override val displayHeight: Int + get() = 104 + + override fun getInputs(recipe: NEUForgeRecipe): Collection<SBItemStack> { + return recipe.inputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: NEUForgeRecipe): Collection<SBItemStack> { + return listOfNotNull(SBItemStack(recipe.outputStack)) + } + + override val icon: ItemStack = ItemStack(Blocks.ANVIL) + override val title: Component = tr("firmament.category.forge", "Forge Recipes") + override val identifier: ResourceLocation = Firmament.identifier("forge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable<NEUForgeRecipe> { + // TODO: theres gotta be an index for these tbh. + return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance<NEUForgeRecipe>() + } + + override val typ: Class<NEUForgeRecipe> + get() = NEUForgeRecipe::class.java +} diff --git a/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt new file mode 100644 index 0000000..c841dd9 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt @@ -0,0 +1,167 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.EntitySpawnReason +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.npc.VillagerProfession +import net.minecraft.world.item.ItemStack +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.entity.EntityRenderer +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.Reforge +import moe.nea.firmament.repo.ReforgeStore +import moe.nea.firmament.repo.RepoItemTypeCache +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.gold +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.tr + +object SBReforgeRecipeRenderer : GenericRecipeRenderer<Reforge> { + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + recipe: Reforge, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack? + ) { + val inputSlot = layouter.createCyclingItemSlot( + bounds.minX + 10, bounds.centerY - 9, + if (mainItem != null) listOf(mainItem) + else generateAllItems(recipe), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + val outputSlut = layouter.createItemSlot( + bounds.minX + 10 + 24 + 24, bounds.centerY - 9, + null, + RecipeLayouter.SlotKind.SMALL_OUTPUT + ) + val statLines = mutableListOf<Pair<String, RecipeLayouter.Updater<Component>>>() + for ((i, statId) in recipe.statUniverse.withIndex()) { + val label = layouter.createLabel( + bounds.minX + 10 + 24 + 24 + 20, bounds.minY + 8 + i * 11, + Component.empty() + ) + statLines.add(statId to label) + } + + fun updateOutput() { + val currentBaseItem = inputSlot.current() + outputSlut.update(currentBaseItem.copy(reforge = recipe.reforgeId)) + val stats = recipe.reforgeStats?.get(currentBaseItem.rarity) ?: mapOf() + for ((stat, label) in statLines) { + label.update( + SBItemStack.Companion.StatLine( + SBItemStack.statIdToName(stat), null, + valueNum = stats[stat] + ).reconstitute(7) + ) + } + } + + if (recipe.reforgeStone != null) { + layouter.createItemSlot( + bounds.minX + 10 + 24, bounds.centerY - 9 - 10, + SBItemStack(recipe.reforgeStone), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + val d = Rectangle( + bounds.minX + 10 + 24, bounds.centerY - 9 + 10, + 16, 16 + ) + layouter.createItemSlot( + d.x, d.y, + SBItemStack(SkyBlockItems.REFORGE_ANVIL), + RecipeLayouter.SlotKind.DISPLAY + ) + layouter.createTooltip( + d, + Rarity.entries.mapNotNull { rarity -> + recipe.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Component.literal("") + .append(rarity.text) + .append(": ") + .append(Component.literal("${formatCommas(cost, 0)} Coins").gold()) + } + ) + } else { + val entity = EntityType.VILLAGER.create(EntityRenderer.fakeWorld, EntitySpawnReason.COMMAND) + ?.also { + it.villagerData = + it.villagerData.withProfession( + MC.currentOrDefaultRegistries, + VillagerProfession.WEAPONSMITH + ) + } + val dim = EntityRenderer.defaultSize + val d = Rectangle( + Point(bounds.minX + 10 + 24 + 8 - dim.width / 2, bounds.centerY - dim.height / 2), + dim + ) + if (entity != null) + layouter.createEntity( + d, + entity + ) + layouter.createTooltip( + d, + tr( + "firmament.recipecategory.reforge.basic", + "This is a basic reforge, available at the Blacksmith." + ).grey() + ) + } + } + + private fun generateAllItems(recipe: Reforge): List<SBItemStack> { + return recipe.eligibleItems.flatMap { + when (it) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> listOf(SBItemStack(it.internalName)) + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> + ReforgeStore.resolveItemType(it.itemType) + .flatMapTo(mutableSetOf()) { itemType -> + listOf(itemType, itemType.dungeonVariant) + } + .flatMapTo(mutableSetOf()) { itemType -> + RepoItemTypeCache.byItemType[itemType] ?: listOf() + } + .map { SBItemStack(it.skyblockId) } + + is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> listOf() + } + } + } + + override fun getInputs(recipe: Reforge): Collection<SBItemStack> { + val reforgeStone = recipe.reforgeStone ?: return emptyList() + return listOf(SBItemStack(reforgeStone)) + } + + override fun getOutputs(recipe: Reforge): Collection<SBItemStack> { + return listOf() + } + + @OptIn(ExpensiveItemCacheApi::class) + override val icon: ItemStack + get() = SBItemStack(SkyBlockItems.REFORGE_ANVIL).asImmutableItemStack() + override val title: Component + get() = tr("firmament.recipecategory.reforge", "Reforge") + override val identifier: ResourceLocation + get() = Firmament.identifier("reforge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable<Reforge> { + return ReforgeStore.allReforges + } + + override val typ: Class<Reforge> + get() = Reforge::class.java +} |
