User: kyle wood Date: 17 Oct 22 23:53 Revision: 1adc227fe44e87347ef414d449b45b3d23e8082b Summary: Use Modrinth API for Architectury creator This also improves handling of coroutines in some places, and it also fixes weird Minecraft version strings being URL encoded from DataProvider for Fabric, which is what fixes #1847. I wanted to use a coroutine-compatible HTTP library, so I pulled in fuel and it seems to work fine. I kept support for configured proxies as well, but I don't really have a way of testing that. Fixes #1892 Fixes #1847 TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=8164&personal=false Index: build.gradle.kts =================================================================== --- build.gradle.kts (revision bd12553dfc310728823c17395a41288cfb604f98) +++ build.gradle.kts (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -85,6 +85,8 @@ implementation(libs.mappingIo) implementation(libs.bundles.asm) + implementation(libs.bundles.fuel) + jflex(libs.jflex.lib) jflexSkeleton(libs.jflex.skeleton) { artifact { Index: gradle/libs.versions.toml =================================================================== --- gradle/libs.versions.toml (revision bd12553dfc310728823c17395a41288cfb604f98) +++ gradle/libs.versions.toml (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -1,7 +1,8 @@ [versions] -coroutines = "1.6.3" +coroutines = "1.6.4" junit = "5.9.0" asm = "9.3" +fuel = "2.3.1" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } @@ -25,6 +26,10 @@ asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" } +fuel = { module = "com.github.kittinunf.fuel:fuel", version.ref = "fuel" } +fuel-json = { module = "com.github.kittinunf.fuel:fuel-json", version.ref = "fuel" } +fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", version.ref = "fuel" } + # Testing test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50" test-mixin = "org.spongepowered:mixin:0.8.5" @@ -37,3 +42,4 @@ [bundles] coroutines = ["coroutines-core", "coroutines-jdk8", "coroutines-swing"] asm = ["asm", "asm-tree", "asm-analysis"] +fuel = ["fuel", "fuel-json", "fuel-coroutines"] Index: src/main/kotlin/creator/PlatformVersion.kt =================================================================== --- src/main/kotlin/creator/PlatformVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/creator/PlatformVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -11,28 +11,39 @@ package com.demonwav.mcdev.creator import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.util.ProxyHttpConnectionFactory +import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.fromJson +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.requests.suspendable +import com.github.kittinunf.fuel.coroutines.awaitString import com.google.gson.Gson import com.intellij.openapi.diagnostic.Attachment import com.intellij.openapi.diagnostic.Logger +import com.intellij.util.proxy.CommonProxy import java.io.IOException +import java.net.Proxy +import java.net.URI import javax.swing.JComboBox +import kotlin.reflect.KClass -private const val cloudflareBaseUrl = "https://minecraftdev.org/versions/" -private const val githubBaseUrl = "https://raw.githubusercontent.com/minecraft-dev/minecraftdev.org/master/versions/" +private const val CLOUDFLARE_BASE_URL = "https://minecraftdev.org/versions/" +private const val GITHUB_BASE_URL = "https://raw.githubusercontent.com/minecraft-dev/minecraftdev.org/master/versions/" val PLATFORM_VERSION_LOGGER = Logger.getInstance("MDev.PlatformVersion") -fun getVersionSelector(type: PlatformType): PlatformVersion { +suspend fun getVersionSelector(type: PlatformType): PlatformVersion { val versionJson = type.versionJson ?: throw UnsupportedOperationException("Incorrect platform type: $type") return getVersionJson(versionJson) } -inline fun getVersionJson(path: String): T { +suspend inline fun getVersionJson(path: String): T { + return getVersionJson(path, T::class) +} + +suspend fun getVersionJson(path: String, type: KClass): T { val text = getText(path) try { - return Gson().fromJson(text) + return Gson().fromJson(text, type) } catch (e: Exception) { val attachment = Attachment("JSON Document", text) attachment.isIncluded = true @@ -41,33 +52,46 @@ } } -fun getText(path: String): String { +suspend fun getText(path: String): String { return try { // attempt cloudflare - doCall(cloudflareBaseUrl + path) + doCall(CLOUDFLARE_BASE_URL + path) } catch (e: IOException) { - PLATFORM_VERSION_LOGGER.warn("Failed to reach cloudflare URL ${cloudflareBaseUrl + path}", e) + PLATFORM_VERSION_LOGGER.warn("Failed to reach cloudflare URL ${CLOUDFLARE_BASE_URL + path}", e) // if that fails, attempt github try { - doCall(githubBaseUrl + path) + doCall(GITHUB_BASE_URL + path) } catch (e: IOException) { - PLATFORM_VERSION_LOGGER.warn("Failed to reach fallback GitHub URL ${githubBaseUrl + path}", e) + PLATFORM_VERSION_LOGGER.warn("Failed to reach fallback GitHub URL ${GITHUB_BASE_URL + path}", e) throw e } } } -private fun doCall(urlText: String): String { - val connection = ProxyHttpConnectionFactory.openHttpConnection(urlText) +private suspend fun doCall(urlText: String): String { + val manager = FuelManager() + manager.proxy = selectProxy(urlText) - connection.setRequestProperty( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/72.0.3626.121 Safari/537.36" - ) + return manager.get(urlText) + .header("User-Agent", "github_org/minecraft-dev/${PluginUtil.pluginVersion}") + .header("Accepts", "application/json") + .suspendable() + .awaitString() +} - return connection.inputStream.use { stream -> stream.reader().use { it.readText() } } +fun selectProxy(urlText: String): Proxy? { + val uri = URI(urlText) + val url = uri.toURL() + + val proxies = CommonProxy.getInstance().select(uri) + for (proxy in proxies) { + try { + url.openConnection(proxy) + return proxy + } catch (_: IOException) {} -} + } + return null +} data class PlatformVersion(var versions: List, var selectedIndex: Int) { fun set(combo: JComboBox) { Index: src/main/kotlin/creator/buildsystem/maven/maven-steps.kt =================================================================== --- src/main/kotlin/creator/buildsystem/maven/maven-steps.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/creator/buildsystem/maven/maven-steps.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -40,6 +40,7 @@ import java.nio.file.StandardOpenOption.CREATE import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING import java.nio.file.StandardOpenOption.WRITE +import kotlinx.coroutines.runBlocking import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel import org.jetbrains.idea.maven.execution.MavenRunConfigurationType import org.jetbrains.idea.maven.execution.MavenRunnerParameters @@ -103,8 +104,10 @@ companion object { val pluginVersions by lazy { + runBlocking { - getVersionJson>("maven.json") - } + getVersionJson>("maven.json") + } + } private val defaultParts = listOf(setupDirs(), setupCore(), setupName(), setupInfo(), setupDependencies()) Index: src/main/kotlin/insight/ListenerLineMarkerProvider.kt =================================================================== --- src/main/kotlin/insight/ListenerLineMarkerProvider.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/insight/ListenerLineMarkerProvider.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -41,10 +41,20 @@ return null } + try { - val identifier = element.toUElementOfType() ?: return null - if (identifier.uastParent !is UMethod || identifier.uastEventListener == null) { - return null - } + val identifier = element.toUElementOfType() ?: return null + if (identifier.uastParent !is UMethod || identifier.uastEventListener == null) { + return null + } + } catch (e: Exception) { + // Kotlin plugin is buggy and can throw exceptions here + // We do the check like this because we don't actually have this class on the classpath + if (e.javaClass.name == "org.jetbrains.kotlin.idea.caches.resolve.KotlinIdeaResolutionException") { + return null + } + // Don't swallow unexpected errors + throw e + } // By this point, we can guarantee that the action of "go to declaration" will work // since the PsiClass can be resolved, meaning the event listener is listening to Index: src/main/kotlin/platform/architectury/creator/ArchitecturyProjectSettingsWizard.kt =================================================================== --- src/main/kotlin/platform/architectury/creator/ArchitecturyProjectSettingsWizard.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/architectury/creator/ArchitecturyProjectSettingsWizard.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -22,7 +22,9 @@ import com.demonwav.mcdev.platform.forge.version.ForgeVersion import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO import com.demonwav.mcdev.util.modUpdateStep +import com.intellij.openapi.diagnostic.Logger import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.EnumComboBoxModel import java.awt.event.ActionListener @@ -37,7 +39,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.swing.Swing @@ -234,6 +236,7 @@ updateMcForm(data) } } catch (e: Exception) { + LOGGER.error("Failed to update versions form", e) error() } @@ -250,9 +253,9 @@ } private suspend fun downloadVersions() = coroutineScope { - val fabricVersionJob = async(Dispatchers.IO) { FabricVersion.downloadData() } - val forgeVersionJob = async(Dispatchers.IO) { ForgeVersion.downloadData() } - val architecturyApiVersionJob = async(Dispatchers.IO) { ArchitecturyVersion.downloadData() } + val fabricVersionJob = asyncIO { FabricVersion.downloadData() } + val forgeVersionJob = asyncIO { ForgeVersion.downloadData() } + val architecturyApiVersionJob = asyncIO { ArchitecturyVersion.downloadData() } versions = ArchitecturyVersions( fabricVersionJob.await() ?: return@coroutineScope, @@ -262,30 +265,43 @@ } private suspend fun updateForm(): Data? = coroutineScope { + try { - val vers = versions ?: return@coroutineScope null + val vers = versions ?: return@coroutineScope null - val selectedVersion = version ?: vers.forgeVersion.sortedMcVersions.firstOrNull() ?: return@coroutineScope null + val selectedVersion = version ?: vers.forgeVersion.sortedMcVersions.firstOrNull() + ?: return@coroutineScope null - val fabricVersionsJob = async(Dispatchers.IO) { vers.fabricVersion.getFabricVersions(selectedVersion) } - val forgeVersionsJob = async(Dispatchers.IO) { vers.forgeVersion.getForgeVersions(selectedVersion) } - val fabricApiVersionsJob = async(Dispatchers.IO) { vers.fabricVersion.getFabricApiVersions(selectedVersion) } - val architecturyApiVersionsJob = async(Dispatchers.IO) { - vers.architecturyVersion.getArchitecturyVersions( - selectedVersion - ) + val fabricVersionsJob = asyncIO { vers.fabricVersion.getFabricVersions(selectedVersion) } + val forgeVersionsJob = asyncIO { vers.forgeVersion.getForgeVersions(selectedVersion) } + val fabricApiVersionsJob = asyncIO { vers.fabricVersion.getFabricApiVersions(selectedVersion) } + val architecturyApiVersionsJob = asyncIO { + vers.architecturyVersion.getArchitecturyVersions(selectedVersion) - } + } - val fabricVersions = fabricVersionsJob.await() - val forgeVersions = forgeVersionsJob.await() - val fabricApiVersions = fabricApiVersionsJob.await() - val architecturyApiVersions = architecturyApiVersionsJob.await() + // awaitAll is better than calling .await() individually + val ( + fabricVersions, + forgeVersions, + fabricApiVersions, + architecturyApiVersions, + ) = listOf( + fabricVersionsJob, + forgeVersionsJob, + fabricApiVersionsJob, + architecturyApiVersionsJob + ).awaitAll() - val data = Data(0, fabricVersions, 0, forgeVersions, 0, fabricApiVersions, 0, architecturyApiVersions, 0) + val data = Data(0, fabricVersions, 0, forgeVersions, 0, fabricApiVersions, 0, architecturyApiVersions, 0) - mcVersionUpdate(data) + mcVersionUpdate(data) - return@coroutineScope data + return@coroutineScope data + } catch (e: Exception) { + // log error manually - something is weird about intellij & coroutine exception handling + LOGGER.error("Error while updating Architectury form version fields", e) + return@coroutineScope null - } + } + } private fun updateMcForm(data: Data) { val vers = versions ?: return @@ -293,9 +309,16 @@ minecraftVersionBox.removeActionListener(minecraftBoxActionListener) minecraftVersionBox.removeAllItems() - minecraftVersionBox.model = CollectionComboBoxModel( - vers.forgeVersion.sortedMcVersions.filter { it >= SemanticVersion.release(1, 16) } - ) + // make copy, so the next 2 operations don't mess up the map + val mcVersions = vers.architecturyVersion.versions.keys.toCollection(LinkedHashSet()) + mcVersions.retainAll(vers.forgeVersion.sortedMcVersions.toSet()) + // Fabric also targets preview versions which aren't semver + // The other option would be to try to parse all of them and catching any exceptions + // But exceptions are slow, so this should be more efficient + val fabricMcVersions = vers.fabricVersion.versions.minecraftVersions.mapTo(HashSet()) { it.name } + mcVersions.retainAll { fabricMcVersions.contains(it.toString()) } + + minecraftVersionBox.model = CollectionComboBoxModel(mcVersions.sortedDescending()) minecraftVersionBox.selectedIndex = data.mcSelectedIndex minecraftVersionBox.addActionListener(minecraftBoxActionListener) } @@ -311,4 +334,8 @@ val architecturyApiVersions: List, val architecturyApiSelectedIndex: Int ) + + companion object { + val LOGGER = Logger.getInstance(ArchitecturyProjectSettingsWizard::class.java) -} + } +} Index: src/main/kotlin/platform/architectury/version/ArchitecturyVersion.kt =================================================================== --- src/main/kotlin/platform/architectury/version/ArchitecturyVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/architectury/version/ArchitecturyVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -10,116 +10,71 @@ package com.demonwav.mcdev.platform.architectury.version +import com.demonwav.mcdev.creator.selectProxy +import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.fromJson +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.requests.suspendable +import com.github.kittinunf.fuel.coroutines.awaitString +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import java.io.IOException -import java.net.URL -import javax.xml.stream.XMLInputFactory -import javax.xml.stream.events.XMLEvent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive class ArchitecturyVersion private constructor( val versions: Map>, - private val mcVersions: MutableList> ) { fun getArchitecturyVersions(mcVersion: SemanticVersion): List { - val roundedVersion = mcVersions.find { mcVersion >= it[0] && mcVersion < it[1] }?.first() return try { - versions[roundedVersion]?.asSequence() - ?.sortedDescending() - ?.take(50) - ?.toList() ?: throw IOException("Could not find any architectury versions for $mcVersion") + val architecturyVersions = versions[mcVersion] ?: throw IOException("Could not find any architectury versions for $mcVersion") + architecturyVersions.take(50) } catch (e: IOException) { e.printStackTrace() emptyList() } } - companion object { - private val updateUrl = URL( - "https://gist.githubusercontent.com" + - "/shedaniel/4a37f350a6e49545347cb798dbfa72b3" + - "/raw/architectury.json" + data class ModrinthVersionApi( + @SerializedName("version_number") + val versionNumber: String, + @SerializedName("game_versions") + val gameVersions: List, + val loaders: List, - ) + ) - suspend fun downloadData(): ArchitecturyVersion? = coroutineScope { - try { - val meta = withContext(Dispatchers.IO) { Json.parseToJsonElement(updateUrl.readText()) } - val versions = mutableMapOf>() - val mcVersions = meta.jsonObject["versions"]?.jsonObject?.map { SemanticVersion.parse(it.key) } - ?.windowed(2, 1)?.toMutableList().also { - it?.add( - listOf( - it.last().last(), - SemanticVersion.parse( - buildString { - append("1.") - append( - when (val part = it.last().last().parts.getOrNull(1)) { - is SemanticVersion.Companion.VersionPart.ReleasePart -> - (part.version + 1).toString() - null -> "?" - else -> part.versionString - } - ) - } - ) - ) - ) - } ?: throw IOException("Could not find any minecraft versions") - meta.jsonObject["versions"]?.jsonObject?.asSequence()?.map { - async(Dispatchers.IO) { - val mcVersion = SemanticVersion.parse(it.key) - URL( - it.value.jsonObject["api"]?.jsonObject?.get("pom")?.jsonPrimitive?.content - ?: throw IOException( - "Could not find pom for $mcVersion" - ) - ) - .openStream().use { stream -> - val inputFactory = XMLInputFactory.newInstance() + companion object { - @Suppress("UNCHECKED_CAST") - val reader = inputFactory.createXMLEventReader(stream) as Iterator - for (event in reader) { - if (!event.isStartElement) { - continue + suspend fun downloadData(): ArchitecturyVersion { + val url = "https://api.modrinth.com/v2/project/architectury-api/version" + val manager = FuelManager() + manager.proxy = selectProxy(url) + + val response = manager.get(url) + .header("User-Agent", PluginUtil.useragent) + .suspendable() + .awaitString() + + val data = Gson().fromJson>(response) + + val apiVersionMap = HashMap>() + + for (version in data) { + val apiVersion = SemanticVersion.parse(version.versionNumber.substringBeforeLast('+')) + + for (gameVersion in version.gameVersions) { + val parsed = SemanticVersion.parse(gameVersion) + val set = apiVersionMap.computeIfAbsent(parsed) { HashSet() } + set += apiVersion - } + } - val start = event.asStartElement() - val name = start.name.localPart - if (name != "version") { - continue - } + } - val versionEvent = reader.next() - if (!versionEvent.isCharacters) { - continue + val apiVersionMapList = HashMap>() + for ((mcVersion, archList) in apiVersionMap.entries) { + apiVersionMapList[mcVersion] = archList.sortedDescending() - } + } - val version = versionEvent.asCharacters().data - val regex = it.value.jsonObject["api"]?.jsonObject?.get("filter") - ?.jsonPrimitive?.content?.toRegex() - ?: throw IOException("Could not find filter for $mcVersion") - if (regex.matches(version)) { - versions.getOrPut(mcVersion) { mutableListOf() } - .add(SemanticVersion.parse(version)) - } - } - } - } - }?.asSequence()?.toList()?.awaitAll() - return@coroutineScope ArchitecturyVersion(versions.toSortedMap(), mcVersions) - } catch (e: IOException) { - e.printStackTrace() - return@coroutineScope null + return ArchitecturyVersion(apiVersionMapList) - } - } - } + } + } +} -} Index: src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt =================================================================== --- src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/fabric/creator/FabricProjectSettingsWizard.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -21,6 +21,7 @@ import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO import com.demonwav.mcdev.util.modUpdateStep import com.demonwav.mcdev.util.toPackageName import com.extracraftx.minecraft.templatemakerfabric.data.DataProvider @@ -46,7 +47,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.swing.Swing @@ -284,18 +285,24 @@ private suspend fun downloadVersions(): DataProvider? = coroutineScope { // prefetch the data val dataProvider = DataProvider() - val minecraftVersionJob = async(Dispatchers.IO) { runCatching { dataProvider.minecraftVersions }.getOrNull() } - val fabricApiVersionJob = async(Dispatchers.IO) { runCatching { dataProvider.fabricApiVersions }.getOrNull() } - val yarnVersionJob = async(Dispatchers.IO) { runCatching { dataProvider.yarnVersions }.getOrNull() } - val loomVersionJob = async(Dispatchers.IO) { runCatching { dataProvider.loomVersions }.getOrNull() } - val loaderVersionJob = async(Dispatchers.IO) { runCatching { dataProvider.loaderVersions }.getOrNull() } + val minecraftVersionJob = asyncIO { runCatching { dataProvider.minecraftVersions }.getOrNull() } + val fabricApiVersionJob = asyncIO { runCatching { dataProvider.fabricApiVersions }.getOrNull() } + val yarnVersionJob = asyncIO { runCatching { dataProvider.yarnVersions }.getOrNull() } + val loomVersionJob = asyncIO { runCatching { dataProvider.loomVersions }.getOrNull() } + val loaderVersionJob = asyncIO { runCatching { dataProvider.loaderVersions }.getOrNull() } - minecraftVersionJob.await() ?: return@coroutineScope null - fabricApiVersionJob.await() ?: return@coroutineScope null - yarnVersionJob.await() ?: return@coroutineScope null - loomVersionJob.await() ?: return@coroutineScope null - loaderVersionJob.await() ?: return@coroutineScope null + val results = listOf( + minecraftVersionJob, + fabricApiVersionJob, + yarnVersionJob, + loomVersionJob, + loaderVersionJob, + ).awaitAll() + if (results.any { it == null }) { + return@coroutineScope null + } + return@coroutineScope dataProvider } Index: src/main/kotlin/platform/forge/creator/ForgeProjectSettingsWizard.kt =================================================================== --- src/main/kotlin/platform/forge/creator/ForgeProjectSettingsWizard.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/forge/creator/ForgeProjectSettingsWizard.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -24,6 +24,7 @@ import com.demonwav.mcdev.util.License import com.demonwav.mcdev.util.MinecraftVersions import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO import com.demonwav.mcdev.util.modUpdateStep import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.EnumComboBoxModel @@ -39,7 +40,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.swing.Swing @@ -230,8 +230,8 @@ } private suspend fun downloadVersions() = coroutineScope { - val mcpVersionJob = async(Dispatchers.IO) { McpVersion.downloadData() } - val forgeVersionJob = async(Dispatchers.IO) { ForgeVersion.downloadData() } + val mcpVersionJob = asyncIO { McpVersion.downloadData() } + val forgeVersionJob = asyncIO { ForgeVersion.downloadData() } versions = ForgeVersions( mcpVersionJob.await() ?: return@coroutineScope, @@ -244,8 +244,8 @@ val selectedVersion = version ?: vers.forgeVersion.sortedMcVersions.firstOrNull() ?: return@coroutineScope null - val mcpVersionListJob = async(Dispatchers.IO) { vers.mcpVersion.getMcpVersionList(selectedVersion) } - val forgeVersionsJob = async(Dispatchers.IO) { vers.forgeVersion.getForgeVersions(selectedVersion) } + val mcpVersionListJob = asyncIO { vers.mcpVersion.getMcpVersionList(selectedVersion) } + val forgeVersionsJob = asyncIO { vers.forgeVersion.getForgeVersions(selectedVersion) } val mcpVersionList = mcpVersionListJob.await() val forgeVersions = forgeVersionsJob.await() Index: src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt =================================================================== --- src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/forge/inspections/sideonly/NewExpressionSideOnlyInspection.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -34,7 +34,7 @@ "use @SideOnly annotated classes either." } - override fun buildFix(vararg infos: Any): InspectionGadgetsFix? { + override fun buildFix(vararg infos: Any?): InspectionGadgetsFix? { val annotation = infos[0] as? PsiAnnotation ?: return null return if (annotation.isWritable) { @@ -80,8 +80,7 @@ var classAnnotated = false - if (containingClassSide !== Side.NONE && containingClassSide !== Side.INVALID - ) { + if (containingClassSide !== Side.NONE && containingClassSide !== Side.INVALID) { if (containingClassSide !== classSide) { registerError(expression, offender.getAnnotation(classAnnotation.annotationName)) } Index: src/main/kotlin/platform/forge/version/ForgeVersion.kt =================================================================== --- src/main/kotlin/platform/forge/version/ForgeVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/forge/version/ForgeVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -10,10 +10,13 @@ package com.demonwav.mcdev.platform.forge.version +import com.demonwav.mcdev.creator.selectProxy +import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.sortVersions +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.requests.suspendable import java.io.IOException -import java.net.URL import javax.xml.stream.XMLInputFactory import javax.xml.stream.events.XMLEvent @@ -51,11 +54,19 @@ } companion object { - fun downloadData(): ForgeVersion? { + suspend fun downloadData(): ForgeVersion? { try { - val url = URL("https://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml") + val url = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml" + val manager = FuelManager() + manager.proxy = selectProxy(url) + + val response = manager.get(url) + .header("User-Agent", PluginUtil.useragent) + .suspendable() + .await() + val result = mutableListOf() - url.openStream().use { stream -> + response.body().toStream().use { stream -> val inputFactory = XMLInputFactory.newInstance() @Suppress("UNCHECKED_CAST") Index: src/main/kotlin/platform/liteloader/creator/LiteLoaderProjectSettingsWizard.kt =================================================================== --- src/main/kotlin/platform/liteloader/creator/LiteLoaderProjectSettingsWizard.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/liteloader/creator/LiteLoaderProjectSettingsWizard.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -21,6 +21,7 @@ import com.demonwav.mcdev.platform.mcp.version.McpVersion import com.demonwav.mcdev.platform.mcp.version.McpVersionEntry import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO import com.demonwav.mcdev.util.invokeLater import com.intellij.openapi.ui.MessageType import com.intellij.openapi.ui.popup.Balloon @@ -40,7 +41,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.withContext @@ -209,11 +210,12 @@ val version = minecraftVersionBox.selectedItem as? SemanticVersion - val mcpVersionJob = async(Dispatchers.IO) { McpVersion.downloadData() } - val liteloaderVersionJob = async(Dispatchers.IO) { LiteLoaderVersion.downloadData() } + val mcpVersionJob = asyncIO { McpVersion.downloadData() } + val liteloaderVersionJob = asyncIO { LiteLoaderVersion.downloadData() } - val mcpVersion = mcpVersionJob.await() ?: return@launch - val liteloaderVersion = liteloaderVersionJob.await() ?: return@launch + val (mcpVersionObj, liteloaderVersionObj) = listOf(mcpVersionJob, liteloaderVersionJob).awaitAll() + val mcpVersion = mcpVersionObj as McpVersion? ?: return@launch + val liteloaderVersion = liteloaderVersionObj as LiteLoaderVersion? ?: return@launch val data = withContext(Dispatchers.IO) { val listVersion = version ?: liteloaderVersion.sortedMcVersions.first() Index: src/main/kotlin/platform/liteloader/version/LiteLoaderVersion.kt =================================================================== --- src/main/kotlin/platform/liteloader/version/LiteLoaderVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/liteloader/version/LiteLoaderVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -10,12 +10,17 @@ package com.demonwav.mcdev.platform.liteloader.version +import com.demonwav.mcdev.creator.selectProxy +import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.fromJson import com.demonwav.mcdev.util.sortVersions +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.requests.suspendable +import com.github.kittinunf.fuel.coroutines.awaitString import com.google.gson.Gson +import com.intellij.openapi.diagnostic.Logger import java.io.IOException -import java.net.URL class LiteLoaderVersion private constructor(private var map: Map<*, *>) { @@ -28,16 +33,25 @@ } companion object { - fun downloadData(): LiteLoaderVersion? { + private val LOGGER = Logger.getInstance(LiteLoaderVersion::class.java) + + suspend fun downloadData(): LiteLoaderVersion? { try { - val text = URL("http://dl.liteloader.com/versions/versions.json").readText() + val url = "https://dl.liteloader.com/versions/versions.json" + val manager = FuelManager() + manager.proxy = selectProxy(url) + val text = manager.get(url) + .header("User-Agent", PluginUtil.useragent) + .suspendable() + .awaitString() + val map = Gson().fromJson>(text) val liteLoaderVersion = LiteLoaderVersion(map) liteLoaderVersion.sortedMcVersions return liteLoaderVersion } catch (e: IOException) { - e.printStackTrace() + LOGGER.error("Failed to download LiteLoader version json", e) } return null } Index: src/main/kotlin/platform/sponge/SpongeVersion.kt =================================================================== --- src/main/kotlin/platform/sponge/SpongeVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/platform/sponge/SpongeVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -26,10 +26,10 @@ } companion object { - fun downloadData(): SpongeVersion? { + suspend fun downloadData(): SpongeVersion? { return try { val text = getText("sponge_v2.json") - Gson().fromJson(text) + Gson().fromJson(text, SpongeVersion::class) } catch (e: Exception) { null } Index: src/main/kotlin/update/PluginUtil.kt =================================================================== --- src/main/kotlin/update/PluginUtil.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/update/PluginUtil.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -25,4 +25,7 @@ val pluginVersion: String get() = plugin.version + + val useragent: String + get() = "github_org/minecraft-dev/$pluginVersion" } Index: src/main/kotlin/util/SemanticVersion.kt =================================================================== --- src/main/kotlin/util/SemanticVersion.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/util/SemanticVersion.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -13,6 +13,7 @@ import com.demonwav.mcdev.util.SemanticVersion.Companion.VersionPart.PreReleasePart import com.demonwav.mcdev.util.SemanticVersion.Companion.VersionPart.ReleasePart import com.demonwav.mcdev.util.SemanticVersion.Companion.VersionPart.TextPart +import java.net.URLDecoder /** * Represents a comparable and generalised "semantic version". @@ -130,7 +131,8 @@ } } - val mainPartAndMetadata = value.split("+", limit = 2) + val decodedValue = value.split('+').joinToString("+") { URLDecoder.decode(it, Charsets.UTF_8) } + val mainPartAndMetadata = decodedValue.split("+", limit = 2) val mainPart = mainPartAndMetadata[0] val metadata = mainPartAndMetadata.getOrNull(1) ?: "" Index: src/main/kotlin/util/coroutine-utils.kt =================================================================== --- src/main/kotlin/util/coroutine-utils.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) +++ src/main/kotlin/util/coroutine-utils.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -0,0 +1,22 @@ +/* + * Minecraft Dev for IntelliJ + * + * https://minecraftdev.org + * + * Copyright (c) 2022 minecraft-dev + * + * MIT License + */ + +package com.demonwav.mcdev.util + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +fun CoroutineScope.asyncIO( + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T +): Deferred = async(Dispatchers.IO, start, block) Index: src/main/kotlin/util/utils.kt =================================================================== --- src/main/kotlin/util/utils.kt (revision bd12553dfc310728823c17395a41288cfb604f98) +++ src/main/kotlin/util/utils.kt (revision 1adc227fe44e87347ef414d449b45b3d23e8082b) @@ -32,6 +32,7 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import java.util.Locale +import kotlin.reflect.KClass import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.runAsync @@ -240,6 +241,7 @@ // Using the ugly TypeToken approach we can use any complex generic signature, including // nested generics inline fun Gson.fromJson(text: String): T = fromJson(text, object : TypeToken() {}.type) +fun Gson.fromJson(text: String, type: KClass): T = fromJson(text, type.java) fun Map.containsAllKeys(vararg keys: K) = keys.all { this.containsKey(it) }