aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/repo
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/repo')
-rw-r--r--src/main/kotlin/repo/BetterRepoRecipeCache.kt5
-rw-r--r--src/main/kotlin/repo/ExpLadder.kt3
-rw-r--r--src/main/kotlin/repo/ExpensiveItemCacheApi.kt8
-rw-r--r--src/main/kotlin/repo/HypixelStaticData.kt58
-rw-r--r--src/main/kotlin/repo/ItemCache.kt212
-rw-r--r--src/main/kotlin/repo/MiningRepoData.kt131
-rw-r--r--src/main/kotlin/repo/ModernOverlaysData.kt41
-rw-r--r--src/main/kotlin/repo/Reforge.kt18
-rw-r--r--src/main/kotlin/repo/ReforgeStore.kt8
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt203
-rw-r--r--src/main/kotlin/repo/RepoManager.kt104
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt82
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt252
-rw-r--r--src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt18
-rw-r--r--src/main/kotlin/repo/recipes/RecipeLayouter.kt43
-rw-r--r--src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt44
-rw-r--r--src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt74
-rw-r--r--src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt88
-rw-r--r--src/main/kotlin/repo/recipes/SBReforgeRecipeRenderer.kt167
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
+}