aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament/repo
diff options
context:
space:
mode:
authornea <nea@nea.moe>2023-05-16 01:23:43 +0200
committernea <nea@nea.moe>2023-05-16 01:23:43 +0200
commitead6762eb1c005914b05f9d3c29f334989c67513 (patch)
treecd1409756be2bc4a93195c31d432fef053afe002 /src/main/kotlin/moe/nea/firmament/repo
parent96c546cc73880a7c502c17aadda6ca84c847692d (diff)
downloadfirmament-ead6762eb1c005914b05f9d3c29f334989c67513.tar.gz
firmament-ead6762eb1c005914b05f9d3c29f334989c67513.tar.bz2
firmament-ead6762eb1c005914b05f9d3c29f334989c67513.zip
Replace references to NEU with Firmament
Diffstat (limited to 'src/main/kotlin/moe/nea/firmament/repo')
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/ItemCache.kt123
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/RepoDownloadManager.kt118
-rw-r--r--src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt102
3 files changed, 343 insertions, 0 deletions
diff --git a/src/main/kotlin/moe/nea/firmament/repo/ItemCache.kt b/src/main/kotlin/moe/nea/firmament/repo/ItemCache.kt
new file mode 100644
index 0000000..bd624b6
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/ItemCache.kt
@@ -0,0 +1,123 @@
+package moe.nea.firmament.repo
+
+import com.mojang.serialization.Dynamic
+import io.github.cottonmc.cotton.gui.client.CottonHud
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUItem
+import java.io.PrintWriter
+import java.nio.file.Path
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlin.io.path.absolutePathString
+import kotlin.io.path.writer
+import net.minecraft.SharedConstants
+import net.minecraft.client.resource.language.I18n
+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.NbtOps
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.LegacyTagParser
+import moe.nea.firmament.util.appendLore
+import moe.nea.firmament.util.skyblockId
+
+object ItemCache : IReloadable {
+ val dfuLog = Path.of("logs/dfulog.txt")
+ private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
+ private val df = Schemas.getFixer()
+ private val dfuHandle = PrintWriter(dfuLog.writer())
+ 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) {
+ if (isFlawless)
+ Firmament.logger.error("Failed to run data fixer an item. Check ${dfuLog.absolutePathString()} for more information")
+ isFlawless = false
+ e.printStackTrace(dfuHandle)
+ null
+ }
+
+ fun brokenItemStack(neuItem: NEUItem?): ItemStack {
+ return ItemStack(Items.PAINTING).apply {
+ setCustomName(Text.literal(neuItem?.displayName ?: "null"))
+ appendLore(listOf(Text.translatable("firmament.repo.brokenitem", neuItem?.skyblockItemId)))
+ }
+ }
+
+ private fun NEUItem.asItemStackNow(): ItemStack {
+ try {
+ val oldItemTag = get10809CompoundTag()
+ val modernItemTag = oldItemTag.transformFrom10809ToModern()
+ ?: return brokenItemStack(this)
+ val itemInstance = ItemStack.fromNbt(modernItemTag)
+ if (itemInstance.nbt?.contains("Enchantments") == true) {
+ itemInstance.enchantments.add(NbtCompound())
+ }
+ return itemInstance
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return brokenItemStack(this)
+ }
+ }
+
+ fun NEUItem?.asItemStack(): ItemStack {
+ if (this == null) return brokenItemStack(null)
+ var s = cache[this.skyblockItemId]
+ if (s == null) {
+ s = asItemStackNow()
+ cache[this.skyblockItemId] = s
+ }
+ return s
+ }
+
+ fun NEUItem.getIdentifier() = skyblockId.identifier
+
+
+ var job: Job? = null
+
+ 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) {
+ CottonHud.remove(RepoManager.progressBar)
+ return@launch
+ }
+ val recacheItems = I18n.translate("firmament.repo.cache")
+ RepoManager.progressBar.reportProgress(recacheItems, 0, items.size)
+ CottonHud.add(RepoManager.progressBar)
+ var i = 0
+ items.values.forEach {
+ it.asItemStack() // Rebuild cache
+ RepoManager.progressBar.reportProgress(recacheItems, i++, items.size)
+ }
+ CottonHud.remove(RepoManager.progressBar)
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/repo/RepoDownloadManager.kt b/src/main/kotlin/moe/nea/firmament/repo/RepoDownloadManager.kt
new file mode 100644
index 0000000..09b246a
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/RepoDownloadManager.kt
@@ -0,0 +1,118 @@
+package moe.nea.firmament.repo
+
+import io.ktor.client.call.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.utils.io.jvm.nio.*
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.Firmament.logger
+import moe.nea.firmament.util.iterate
+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 kotlin.io.path.*
+
+
+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? {
+ val response =
+ Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.data.user}/${RepoManager.data.repo}/commits/${RepoManager.data.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.data.user}/${RepoManager.data.repo}/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("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
+ throw RuntimeException("Not Enough Updates detected an invalid zip file. This is a potential security risk, please report this in the Moulberry discord.")
+ }
+ extractedLocation.parent.createDirectories()
+ cis.copyTo(extractedLocation.outputStream())
+ cis.closeEntry()
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt b/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt
new file mode 100644
index 0000000..ac880b5
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/repo/RepoManager.kt
@@ -0,0 +1,102 @@
+package moe.nea.firmament.repo
+
+import io.github.cottonmc.cotton.gui.client.CottonHud
+import io.github.moulberry.repo.NEURecipeCache
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.NEURepositoryException
+import io.github.moulberry.repo.data.NEURecipe
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+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.hud.ProgressBar
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.data.DataHolder
+
+object RepoManager : DataHolder<RepoManager.Config>(serializer(), "repo", ::Config) {
+ @Serializable
+ data class Config(
+ var user: String = "NotEnoughUpdates",
+ var repo: String = "NotEnoughUpdates-REPO",
+ var autoUpdate: Boolean = true,
+ var branch: String = "dangerous",
+ )
+
+ val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
+
+ var recentlyFailedToUpdateItemList = false
+
+ val progressBar = ProgressBar("", null, 0).also {
+ it.setSize(180, 22)
+ }
+
+ val neuRepo: NEURepository = NEURepository.of(RepoDownloadManager.repoSavedLocation).apply {
+ registerReloadListener(ItemCache)
+ registerReloadListener {
+ if (!trySendClientboundUpdateRecipesPacket()) {
+ logger.warn("Failed to issue a ClientboundUpdateRecipesPacket (to reload REI). This may lead to an outdated item list.")
+ recentlyFailedToUpdateItemList = true
+ }
+ }
+ }
+
+ private val recipeCache = NEURecipeCache.forRepo(neuRepo)
+
+ fun getAllRecipes() = neuRepo.items.items.values.asSequence().flatMap { it.recipes }
+
+ fun getRecipesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.recipes[skyblockId.neuItem] ?: setOf()
+ fun getUsagesFor(skyblockId: SkyblockId): Set<NEURecipe> = recipeCache.usages[skyblockId.neuItem] ?: 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) = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem)
+
+ fun launchAsyncUpdate(force: Boolean = false) {
+ Firmament.coroutineScope.launch {
+ progressBar.reportProgress("Downloading", 0, null)
+ CottonHud.add(progressBar)
+ RepoDownloadManager.downloadUpdate(force)
+ progressBar.reportProgress("Download complete", 1, 1)
+ reload()
+ }
+ }
+
+ fun reload() {
+ try {
+ progressBar.reportProgress("Reloading from Disk", 0, null)
+ CottonHud.add(progressBar)
+ neuRepo.reload()
+ } catch (exc: NEURepositoryException) {
+ MinecraftClient.getInstance().player?.sendMessage(
+ Text.literal("Failed to reload repository. This will result in some mod features not working.")
+ )
+ CottonHud.remove(progressBar)
+ exc.printStackTrace()
+ }
+ }
+
+ fun initialize() {
+ if (data.autoUpdate) {
+ launchAsyncUpdate()
+ } else {
+ reload()
+ }
+ }
+
+}