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.kt28
-rw-r--r--src/main/kotlin/repo/EssenceRecipeProvider.kt50
-rw-r--r--src/main/kotlin/repo/ExpLadder.kt94
-rw-r--r--src/main/kotlin/repo/HypixelStaticData.kt107
-rw-r--r--src/main/kotlin/repo/ItemCache.kt215
-rw-r--r--src/main/kotlin/repo/ItemNameLookup.kt98
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt128
-rw-r--r--src/main/kotlin/repo/RepoManager.kt145
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt126
9 files changed, 991 insertions, 0 deletions
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
new file mode 100644
index 0000000..91a6b50
--- /dev/null
+++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
@@ -0,0 +1,28 @@
+
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEURecipe
+import moe.nea.firmament.util.SkyblockId
+
+class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) : IReloadable {
+ var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+ var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+
+ override fun reload(repository: NEURepository) {
+ val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val baseRecipes = repository.items.items.values
+ .asSequence()
+ .flatMap { it.recipes }
+ val extraRecipes = essenceRecipeProvider.recipes
+ (baseRecipes + extraRecipes)
+ .forEach { recipe ->
+ recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
+ }
+ this.usages = usages
+ this.recipes = recipes
+ }
+}
diff --git a/src/main/kotlin/repo/EssenceRecipeProvider.kt b/src/main/kotlin/repo/EssenceRecipeProvider.kt
new file mode 100644
index 0000000..1833258
--- /dev/null
+++ b/src/main/kotlin/repo/EssenceRecipeProvider.kt
@@ -0,0 +1,50 @@
+
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUIngredient
+import io.github.moulberry.repo.data.NEURecipe
+import moe.nea.firmament.util.SkyblockId
+
+class EssenceRecipeProvider : IReloadable {
+ data class EssenceUpgradeRecipe(
+ val itemId: SkyblockId,
+ val starCountAfter: Int,
+ val essenceCost: Int,
+ val essenceType: String, // TODO: replace with proper type
+ val extraItems: List<NEUIngredient>,
+ ) : NEURecipe {
+ val essenceIngredient= NEUIngredient.fromString("${essenceType}:$essenceCost")
+ val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+
+ override fun getAllInputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
+ }
+
+ override fun getAllOutputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
+ }
+ }
+
+ var recipes = listOf<EssenceUpgradeRecipe>()
+ private set
+
+ override fun reload(repository: NEURepository) {
+ val recipes = mutableListOf<EssenceUpgradeRecipe>()
+ for ((neuId, costs) in repository.constants.essenceCost.costs) {
+ // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
+ for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
+ val items = costs.itemCosts[starCountAfter] ?: emptyList()
+ recipes.add(
+ EssenceUpgradeRecipe(
+ SkyblockId(neuId),
+ starCountAfter,
+ essenceCost,
+ "ESSENCE_" + costs.type.uppercase(), // how flimsy
+ items.map { NEUIngredient.fromString(it) }))
+ }
+ }
+ this.recipes = recipes
+ }
+}
diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt
new file mode 100644
index 0000000..fbc9eb8
--- /dev/null
+++ b/src/main/kotlin/repo/ExpLadder.kt
@@ -0,0 +1,94 @@
+
+
+package moe.nea.firmament.repo
+
+import com.google.common.cache.CacheBuilder
+import com.google.common.cache.CacheLoader
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.constants.PetLevelingBehaviourOverride
+import io.github.moulberry.repo.data.Rarity
+
+object ExpLadders : IReloadable {
+
+ data class PetLevel(
+ val currentLevel: Int,
+ val maxLevel: Int,
+ val expRequiredForNextLevel: Long,
+ val expRequiredForMaxLevel: Long,
+ val expInCurrentLevel: Float,
+ var expTotal: Float,
+ ) {
+ val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel
+ }
+
+ data class ExpLadder(
+ val individualLevelCost: List<Long>,
+ ) {
+ val cumulativeLevelCost = individualLevelCost.runningFold(0F) { a, b -> a + b }.map { it.toLong() }
+ fun getPetLevel(currentExp: Double): PetLevel {
+ val currentOneIndexedLevel = cumulativeLevelCost.indexOfLast { it <= currentExp } + 1
+ val expForNextLevel = if (currentOneIndexedLevel > individualLevelCost.size) { // Max leveled pet
+ individualLevelCost.last()
+ } else {
+ individualLevelCost[currentOneIndexedLevel - 1]
+ }
+ val expInCurrentLevel =
+ if (currentOneIndexedLevel >= cumulativeLevelCost.size)
+ currentExp.toFloat() - cumulativeLevelCost.last()
+ else
+ (expForNextLevel - (cumulativeLevelCost[currentOneIndexedLevel] - currentExp.toFloat())).coerceAtLeast(
+ 0F
+ )
+ return PetLevel(
+ currentLevel = currentOneIndexedLevel,
+ maxLevel = cumulativeLevelCost.size,
+ expRequiredForNextLevel = expForNextLevel,
+ expRequiredForMaxLevel = cumulativeLevelCost.last(),
+ expInCurrentLevel = expInCurrentLevel,
+ expTotal = currentExp.toFloat()
+ )
+ }
+
+ fun getPetExpForLevel(level: Int): Long {
+ if (level < 2) return 0L
+ if (level >= cumulativeLevelCost.size) return cumulativeLevelCost.last()
+ return cumulativeLevelCost[level - 1]
+ }
+ }
+
+ private data class Key(val petIdWithoutRarity: String, val rarity: Rarity)
+
+ private val expLadders = CacheBuilder.newBuilder()
+ .build(object : CacheLoader<Key, ExpLadder>() {
+ override fun load(key: Key): ExpLadder {
+ val pld = RepoManager.neuRepo.constants.petLevelingData
+ var exp = pld.petExpCostForLevel
+ var offset = pld.petLevelStartOffset[key.rarity]!!
+ var maxLevel = 100
+ val override = pld.petLevelingBehaviourOverrides[key.petIdWithoutRarity]
+ if (override != null) {
+ maxLevel = override.maxLevel ?: maxLevel
+ offset = override.petLevelStartOffset?.get(key.rarity) ?: offset
+ when (override.petExpCostModifierType) {
+ PetLevelingBehaviourOverride.PetExpModifierType.APPEND ->
+ exp = exp + override.petExpCostModifier
+
+ PetLevelingBehaviourOverride.PetExpModifierType.REPLACE ->
+ exp = override.petExpCostModifier
+
+ null -> {}
+ }
+ }
+ return ExpLadder(exp.drop(offset).take(maxLevel - 1).map { it.toLong() })
+ }
+ })
+
+ override fun reload(repository: NEURepository?) {
+ expLadders.invalidateAll()
+ }
+
+ fun getExpLadder(petId: String, rarity: Rarity): ExpLadder {
+ return expLadders.get(Key(petId, rarity))
+ }
+}
diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt
new file mode 100644
index 0000000..5c2a2fc
--- /dev/null
+++ b/src/main/kotlin/repo/HypixelStaticData.kt
@@ -0,0 +1,107 @@
+
+
+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.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
+
+object HypixelStaticData {
+ private val logger = LogManager.getLogger("Firmament.HypixelStaticData")
+ private val moulberryBaseUrl = "https://moulberry.codes"
+ private val hypixelApiBaseUrl = "https://api.hypixel.net"
+ var lowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var bazaarData: Map<SkyblockId, BazaarData> = mapOf()
+ private set
+ var collectionData: Map<String, CollectionSkillData> = mapOf()
+ private set
+
+ @Serializable
+ data class BazaarData(
+ @SerialName("product_id")
+ val productId: SkyblockId.BazaarStock,
+ @SerialName("quick_status")
+ val quickStatus: BazaarStatus,
+ )
+
+ @Serializable
+ data class BazaarStatus(
+ val sellPrice: Double,
+ val sellVolume: Long,
+ val sellMovingWeek: Long,
+ val sellOrders: Long,
+ val buyPrice: Double,
+ val buyVolume: Long,
+ val buyMovingWeek: Long,
+ val buyOrders: Long
+ )
+
+ @Serializable
+ private data class BazaarResponse(
+ val success: Boolean,
+ val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(),
+ )
+
+ fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item]
+
+
+ fun spawnDataCollectionLoop() {
+ Firmament.coroutineScope.launch {
+ logger.info("Updating collection data")
+ updateCollectionData()
+ }
+ Firmament.coroutineScope.launch {
+ while (true) {
+ logger.info("Updating NEU prices")
+ updatePrices()
+ withTimeoutOrNull(10.minutes) { waitForInput(IKeyBinding.ofKeyCode(GLFW.GLFW_KEY_U)) }
+ }
+ }
+ }
+
+ 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>>()
+ }
+
+ private suspend fun fetchBazaarPrices() {
+ val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>()
+ if (!response.success) {
+ logger.warn("Retrieved unsuccessful bazaar data")
+ }
+ bazaarData = response.products.mapKeys { it.key.toRepoId() }
+ }
+
+ private suspend fun updateCollectionData() {
+ val response =
+ Firmament.httpClient.get("$hypixelApiBaseUrl/resources/skyblock/collections").body<CollectionResponse>()
+ if (!response.success) {
+ logger.warn("Retrieved unsuccessful collection data")
+ }
+ collectionData = response.collections
+ logger.info("Downloaded ${collectionData.values.sumOf { it.items.values.size }} collections")
+ }
+
+}
diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt
new file mode 100644
index 0000000..08143be
--- /dev/null
+++ b/src/main/kotlin/repo/ItemCache.kt
@@ -0,0 +1,215 @@
+
+
+package moe.nea.firmament.repo
+
+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.launch
+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.nbt.NbtOps
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.gui.config.HudMeta
+import moe.nea.firmament.gui.config.HudPosition
+import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.util.LegacyTagParser
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.appendLore
+import moe.nea.firmament.util.item.setCustomName
+import moe.nea.firmament.util.item.setSkullOwner
+import moe.nea.firmament.util.modifyLore
+import moe.nea.firmament.util.skyblockId
+
+object ItemCache : IReloadable {
+ private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
+ private val df = Schemas.getFixer()
+ val logger = LogManager.getLogger("${Firmament.logger.name}.ItemCache")
+ var isFlawless = true
+ private set
+
+ private fun NEUItem.get10809CompoundTag(): NbtCompound = NbtCompound().apply {
+ put("tag", LegacyTagParser.parse(nbttag))
+ putString("id", minecraftItemId)
+ putByte("Count", 1)
+ putShort("Damage", damage.toShort())
+ }
+
+ private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? =
+ try {
+ df.update(
+ TypeReferences.ITEM_STACK,
+ Dynamic(NbtOps.INSTANCE, this),
+ -1,
+ SharedConstants.getGameVersion().saveVersion.id
+ ).value as NbtCompound
+ } catch (e: Exception) {
+ isFlawless = false
+ logger.error("Could not data fix up $this", e)
+ null
+ }
+
+ fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack {
+ return ItemStack(Items.PAINTING).apply {
+ setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null"))
+ appendLore(
+ listOf(
+ Text.stringifiedTranslatable(
+ "firmament.repo.brokenitem",
+ (neuItem?.skyblockItemId ?: idHint)
+ )
+ )
+ )
+ }
+ }
+
+ private fun NEUItem.asItemStackNow(): ItemStack {
+ try {
+ val oldItemTag = get10809CompoundTag()
+ val modernItemTag = oldItemTag.transformFrom10809ToModern()
+ ?: return brokenItemStack(this)
+ val itemInstance =
+ ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
+ 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()
+ return brokenItemStack(this)
+ }
+ }
+
+ fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map<String, String>? = null): ItemStack {
+ if (this == null) return brokenItemStack(null, idHint)
+ var s = cache[this.skyblockItemId]
+ if (s == null) {
+ s = asItemStackNow()
+ cache[this.skyblockItemId] = s
+ }
+ if (!loreReplacements.isNullOrEmpty()) {
+ s = s.copy()!!
+ s.applyLoreReplacements(loreReplacements)
+ s.setCustomName(s.name.applyLoreReplacements(loreReplacements))
+ }
+ return s
+ }
+
+ fun ItemStack.applyLoreReplacements(loreReplacements: Map<String, String>) {
+ modifyLore { lore ->
+ lore.map {
+ it.applyLoreReplacements(loreReplacements)
+ }
+ }
+ }
+
+ fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text {
+ assert(this.siblings.isEmpty())
+ var string = this.string
+ loreReplacements.forEach { (find, replace) ->
+ string = string.replace("{$find}", replace)
+ }
+ return Text.literal(string).styled { this.style }
+ }
+
+ fun NEUItem.getIdentifier() = skyblockId.identifier
+
+ 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
+ }
+
+ @get:Bind("current")
+ var current: Double = 0.0
+
+ @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()
+ }
+ }
+
+ override fun reload(repository: NEURepository) {
+ val j = job
+ if (j != null && j.isActive) {
+ j.cancel()
+ }
+ cache.clear()
+ isFlawless = true
+
+ 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
+ }
+ }
+
+ fun coinItem(coinAmount: Int): ItemStack {
+ var uuid = UUID.fromString("2070f6cb-f5db-367a-acd0-64d39a7e5d1b")
+ var texture =
+ "http://textures.minecraft.net/texture/538071721cc5b4cd406ce431a13f86083a8973e1064d2f8897869930ee6e5237"
+ if (coinAmount >= 100000) {
+ uuid = UUID.fromString("94fa2455-2881-31fe-bb4e-e3e24d58dbe3")
+ texture =
+ "http://textures.minecraft.net/texture/c9b77999fed3a2758bfeaf0793e52283817bea64044bf43ef29433f954bb52f6"
+ }
+ if (coinAmount >= 10000000) {
+ uuid = UUID.fromString("0af8df1f-098c-3b72-ac6b-65d65fd0b668")
+ texture =
+ "http://textures.minecraft.net/texture/7b951fed6a7b2cbc2036916dec7a46c4a56481564d14f945b6ebc03382766d3b"
+ }
+ val itemStack = ItemStack(Items.PLAYER_HEAD)
+ itemStack.setCustomName(Text.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins"))
+ itemStack.setSkullOwner(uuid, texture)
+ return itemStack
+ }
+}
+
+
+operator fun NbtCompound.set(key: String, value: String) {
+ putString(key, value)
+}
+
+operator fun NbtCompound.set(key: String, value: NbtElement) {
+ put(key, value)
+}
diff --git a/src/main/kotlin/repo/ItemNameLookup.kt b/src/main/kotlin/repo/ItemNameLookup.kt
new file mode 100644
index 0000000..770de85
--- /dev/null
+++ b/src/main/kotlin/repo/ItemNameLookup.kt
@@ -0,0 +1,98 @@
+
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUItem
+import java.util.NavigableMap
+import java.util.TreeMap
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.removeColorCodes
+import moe.nea.firmament.util.skyblockId
+
+object ItemNameLookup : IReloadable {
+
+ fun getItemNameChunks(name: String): Set<String> {
+ return name.removeColorCodes().split(" ").filterTo(mutableSetOf()) { it.isNotBlank() }
+ }
+
+ var nameMap: NavigableMap<String, out Set<SkyblockId>> = TreeMap()
+
+ override fun reload(repository: NEURepository) {
+ val nameMap = TreeMap<String, MutableSet<SkyblockId>>()
+ repository.items.items.values.forEach { item ->
+ getAllNamesForItem(item).forEach { name ->
+ val chunks = getItemNameChunks(name)
+ chunks.forEach { chunk ->
+ val set = nameMap.getOrPut(chunk, ::mutableSetOf)
+ set.add(item.skyblockId)
+ }
+ }
+ }
+ this.nameMap = nameMap
+ }
+
+ fun getAllNamesForItem(item: NEUItem): Set<String> {
+ val names = mutableSetOf<String>()
+ names.add(item.displayName)
+ if (item.displayName.contains("Enchanted Book")) {
+ val enchantName = item.lore.firstOrNull()
+ if (enchantName != null) {
+ names.add(enchantName)
+ }
+ }
+ return names
+ }
+
+ fun findItemCandidatesByName(name: String): MutableSet<SkyblockId> {
+ val candidates = mutableSetOf<SkyblockId>()
+ for (chunk in getItemNameChunks(name)) {
+ val set = nameMap[chunk] ?: emptySet()
+ candidates.addAll(set)
+ }
+ return candidates
+ }
+
+
+ fun guessItemByName(
+ /**
+ * The display name of the item. Color codes will be ignored.
+ */
+ name: String,
+ /**
+ * Whether the [name] may contain other text, such as reforges, master stars and such.
+ */
+ mayBeMangled: Boolean
+ ): SkyblockId? {
+ val cleanName = name.removeColorCodes()
+ return findBestItemFromCandidates(
+ findItemCandidatesByName(cleanName),
+ cleanName,
+ true
+ )
+ }
+
+ fun findBestItemFromCandidates(
+ candidates: Iterable<SkyblockId>,
+ name: String, mayBeMangled: Boolean
+ ): SkyblockId? {
+ val expectedClean = name.removeColorCodes()
+ var bestMatch: SkyblockId? = null
+ var bestMatchLength = -1
+ for (candidate in candidates) {
+ val item = RepoManager.getNEUItem(candidate) ?: continue
+ for (name in getAllNamesForItem(item)) {
+ val actualClean = name.removeColorCodes()
+ val matches = if (mayBeMangled) expectedClean == actualClean
+ else expectedClean.contains(actualClean)
+ if (!matches) continue
+ if (actualClean.length > bestMatchLength) {
+ bestMatch = candidate
+ bestMatchLength = actualClean.length
+ }
+ }
+ }
+ return bestMatch
+ }
+
+}
diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt
new file mode 100644
index 0000000..d674f23
--- /dev/null
+++ b/src/main/kotlin/repo/RepoDownloadManager.kt
@@ -0,0 +1,128 @@
+
+
+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.jvm.nio.copyTo
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
+import java.util.zip.ZipInputStream
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import kotlin.io.path.createDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.inputStream
+import kotlin.io.path.outputStream
+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.util.iterate
+
+
+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) }
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt
new file mode 100644
index 0000000..f0da397
--- /dev/null
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -0,0 +1,145 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.NEURepositoryException
+import io.github.moulberry.repo.data.NEUItem
+import io.github.moulberry.repo.data.NEURecipe
+import io.github.moulberry.repo.data.Rarity
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
+import kotlinx.coroutines.launch
+import net.minecraft.client.MinecraftClient
+import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
+import net.minecraft.text.Text
+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.rei.PetData
+import moe.nea.firmament.util.MinecraftDispatcher
+import moe.nea.firmament.util.SkyblockId
+
+object RepoManager {
+ object Config : ManagedConfig("repo") {
+ var username by string("username") { "NotEnoughUpdates" }
+ var reponame by string("reponame") { "NotEnoughUpdates-REPO" }
+ var branch by string("branch") { "master" }
+ val autoUpdate by toggle("autoUpdate") { true }
+ val reset by button("reset") {
+ username = "NotEnoughUpdates"
+ reponame = "NotEnoughUpdates-REPO"
+ branch = "master"
+ save()
+ }
+
+ val disableItemGroups by toggle("disable-item-groups") { true }
+ val reload by button("reload") {
+ save()
+ RepoManager.reload()
+ }
+ val redownload by button("redownload") {
+ save()
+ RepoManager.launchAsyncUpdate(true)
+ }
+ }
+
+ val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
+
+ var recentlyFailedToUpdateItemList = false
+
+ val neuRepo: NEURepository = NEURepository.of(RepoDownloadManager.repoSavedLocation).apply {
+ registerReloadListener(ItemCache)
+ registerReloadListener(ExpLadders)
+ registerReloadListener(ItemNameLookup)
+ ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this))
+ registerReloadListener {
+ Firmament.coroutineScope.launch(MinecraftDispatcher) {
+ if (!trySendClientboundUpdateRecipesPacket()) {
+ logger.warn("Failed to issue a ClientboundUpdateRecipesPacket (to reload REI). This may lead to an outdated item list.")
+ recentlyFailedToUpdateItemList = true
+ }
+ }
+ }
+ }
+
+ val essenceRecipeProvider = EssenceRecipeProvider()
+ val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider)
+
+ init {
+ neuRepo.registerReloadListener(essenceRecipeProvider)
+ neuRepo.registerReloadListener(recipeCache)
+ }
+
+ fun getAllRecipes() = neuRepo.items.items.values.asSequence().flatMap { it.recipes }
+
+ fun getRecipesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.recipes[skyblockId] ?: setOf()
+ fun getUsagesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.usages[skyblockId] ?: setOf()
+
+ private fun trySendClientboundUpdateRecipesPacket(): Boolean {
+ return MinecraftClient.getInstance().world != null && MinecraftClient.getInstance().networkHandler?.onSynchronizeRecipes(
+ SynchronizeRecipesS2CPacket(mutableListOf())
+ ) != null
+ }
+
+ init {
+ ClientTickEvents.START_WORLD_TICK.register(ClientTickEvents.StartWorldTick {
+ if (recentlyFailedToUpdateItemList && trySendClientboundUpdateRecipesPacket())
+ recentlyFailedToUpdateItemList = false
+ })
+ }
+
+ fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem)
+
+ fun launchAsyncUpdate(force: Boolean = false) {
+ Firmament.coroutineScope.launch {
+ ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper boundy bar
+ ItemCache.ReloadProgressHud.isEnabled = true
+ try {
+ RepoDownloadManager.downloadUpdate(force)
+ ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1)
+ } finally {
+ ItemCache.ReloadProgressHud.isEnabled = false
+ }
+ reload()
+ }
+ }
+
+ fun reload() {
+ try {
+ ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk",
+ 0,
+ -1) // TODO: replace with a proper boundy bar
+ ItemCache.ReloadProgressHud.isEnabled = true
+ neuRepo.reload()
+ } catch (exc: NEURepositoryException) {
+ MinecraftClient.getInstance().player?.sendMessage(
+ Text.literal("Failed to reload repository. This will result in some mod features not working.")
+ )
+ ItemCache.ReloadProgressHud.isEnabled = false
+ exc.printStackTrace()
+ }
+ }
+
+ fun initialize() {
+ if (Config.autoUpdate) {
+ launchAsyncUpdate()
+ } else {
+ reload()
+ }
+ }
+
+ fun getPotentialStubPetData(skyblockId: SkyblockId): PetData? {
+ val parts = skyblockId.neuItem.split(";")
+ if (parts.size != 2) {
+ return null
+ }
+ val (petId, rarityIndex) = parts
+ if (!rarityIndex.all { it.isDigit() }) {
+ return null
+ }
+ val intIndex = rarityIndex.toInt()
+ if (intIndex !in Rarity.values().indices) return null
+ if (petId !in neuRepo.constants.petNumbers) return null
+ return PetData(Rarity.values()[intIndex], petId, 0.0, true)
+ }
+
+}
diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt
new file mode 100644
index 0000000..f92fe4f
--- /dev/null
+++ b/src/main/kotlin/repo/RepoModResourcePack.kt
@@ -0,0 +1,126 @@
+
+package moe.nea.firmament.repo
+
+import java.io.InputStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.*
+import net.fabricmc.fabric.api.resource.ModResourcePack
+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.ResourceMetadataReader
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.PathUtil
+import moe.nea.firmament.Firmament
+
+class RepoModResourcePack(val basePath: Path) : ModResourcePack {
+ companion object {
+ fun append(packs: MutableList<in ModResourcePack>) {
+ Firmament.logger.info("Registering mod resource pack")
+ packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
+ }
+
+ fun createResourceDirectly(identifier: Identifier): Optional<Resource> {
+ val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation)
+ return Optional.of(
+ Resource(
+ pack,
+ pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty()
+ ) {
+ val base =
+ pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta"))
+ if (base == null)
+ ResourceMetadata.NONE
+ else
+ NamespaceResourceManager.loadMetadata(base)
+ }
+ )
+ }
+ }
+
+ override fun close() {
+ }
+
+ override fun openRoot(vararg segments: String): InputSupplier<InputStream>? {
+ return getFile(segments)?.let { InputSupplier.create(it) }
+ }
+
+ fun getFile(segments: Array<out String>): Path? {
+ PathUtil.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
+ if (id.namespace != "neurepo") return null
+ val file = getFile(id.path.split("/").toTypedArray())
+ return file?.let { InputSupplier.create(it) }
+ }
+
+ override fun findResources(
+ type: ResourceType?,
+ namespace: String,
+ prefix: String,
+ consumer: ResourcePack.ResultConsumer
+ ) {
+ if (namespace != "neurepo") return
+ if (type != ResourceType.CLIENT_RESOURCES) return
+
+ val prefixPath = basePath.resolve(prefix)
+ if (!prefixPath.exists())
+ return
+ Files.walk(prefixPath)
+ .asSequence()
+ .map { it.relativeTo(basePath) }
+ .forEach {
+ consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it))
+ }
+ }
+
+ override fun getNamespaces(type: ResourceType?): Set<String> {
+ if (type != ResourceType.CLIENT_RESOURCES) return emptySet()
+ return setOf("neurepo")
+ }
+
+ override fun <T> parseMetadata(metaReader: ResourceMetadataReader<T>): T? {
+ return AbstractFileResourcePack.parseMetadata(
+ metaReader, """
+{
+ "pack": {
+ "pack_format": 12,
+ "description": "NEU Repo Resources"
+ }
+}
+""".trimIndent().byteInputStream()
+ )
+ }
+
+ override fun getInfo(): ResourcePackInfo {
+ return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty())
+ }
+
+ override fun getFabricModMetadata(): ModMetadata {
+ return FabricLoader.getInstance().getModContainer("firmament")
+ .get().metadata
+ }
+
+ override fun createOverlay(overlay: String): ModResourcePack {
+ return RepoModResourcePack(basePath.resolve(overlay))
+ }
+}