Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
This commit is contained in:
28
src/main/kotlin/repo/BetterRepoRecipeCache.kt
Normal file
28
src/main/kotlin/repo/BetterRepoRecipeCache.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
50
src/main/kotlin/repo/EssenceRecipeProvider.kt
Normal file
50
src/main/kotlin/repo/EssenceRecipeProvider.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
94
src/main/kotlin/repo/ExpLadder.kt
Normal file
94
src/main/kotlin/repo/ExpLadder.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
107
src/main/kotlin/repo/HypixelStaticData.kt
Normal file
107
src/main/kotlin/repo/HypixelStaticData.kt
Normal file
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
215
src/main/kotlin/repo/ItemCache.kt
Normal file
215
src/main/kotlin/repo/ItemCache.kt
Normal file
@@ -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)
|
||||
}
|
||||
98
src/main/kotlin/repo/ItemNameLookup.kt
Normal file
98
src/main/kotlin/repo/ItemNameLookup.kt
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
128
src/main/kotlin/repo/RepoDownloadManager.kt
Normal file
128
src/main/kotlin/repo/RepoDownloadManager.kt
Normal file
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
145
src/main/kotlin/repo/RepoManager.kt
Normal file
145
src/main/kotlin/repo/RepoManager.kt
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
126
src/main/kotlin/repo/RepoModResourcePack.kt
Normal file
126
src/main/kotlin/repo/RepoModResourcePack.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user