Refactor source layout

Introduce compat source sets and move all kotlin sources to the main directory

[no changelog]
This commit is contained in:
Linnea Gräf
2024-08-28 19:04:24 +02:00
parent a690630816
commit d2f240ff0c
251 changed files with 295 additions and 38 deletions

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

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

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

View 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")
}
}

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

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

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

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

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