diff --git a/gradle.properties b/gradle.properties index 537509d91..7d69fb134 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 241-EAP-SNAPSHOT ideaVersionName = 2024.1 -coreVersion = 1.6.12 +coreVersion = 1.7.0 downloadIdeaSources = true pluginTomlVersion = 241.8102.131 diff --git a/readme.md b/readme.md index b60cc3d45..9d5805c99 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ Minecraft Development for IntelliJ -Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.6.12-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) +Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.7.0-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) ---------------------- @@ -137,6 +137,7 @@ Supported Platforms - [![Sponge Icon](src/main/resources/assets/icons/platform/Sponge_dark.png?raw=true) **Sponge**](https://www.spongepowered.org/) - [![Architectury Icon](src/main/resources/assets/icons/platform/Architectury.png?raw=true) **Architectury**](https://github.com/architectury/architectury-api) - [![Forge Icon](src/main/resources/assets/icons/platform/Forge.png?raw=true) **Minecraft Forge**](https://forums.minecraftforge.net/) +- Neoforge - [![Fabric Icon](src/main/resources/assets/icons/platform/Fabric.png?raw=true) **Fabric**](https://fabricmc.net) - [![Mixins Icon](src/main/resources/assets/icons/platform/Mixins_dark.png?raw=true) **Mixins**](https://github.com/SpongePowered/Mixin) - [![BungeeCord Icon](src/main/resources/assets/icons/platform/BungeeCord.png?raw=true) **BungeeCord**](https://www.spigotmc.org/wiki/bungeecord/) ([![Waterfall Icon](src/main/resources/assets/icons/platform/Waterfall.png?raw=true) Waterfall](https://github.com/PaperMC/Waterfall)) diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelBuilderImpl.groovy new file mode 100644 index 000000000..027f7cae6 --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelBuilderImpl.groovy @@ -0,0 +1,72 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mcp.gradle.tooling.neogradle + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 +import org.gradle.api.Project +import org.jetbrains.annotations.NotNull +import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder +import org.jetbrains.plugins.gradle.tooling.ModelBuilderService + +final class NeoGradle7ModelBuilderImpl implements ModelBuilderService { + + @Override + boolean canBuild(String modelName) { + return McpModelNG7.name == modelName + } + + @Override + Object buildAll(String modelName, Project project) { + def extension = project.extensions.findByName('minecraft') + if (extension == null) { + return null + } + + if (!project.plugins.findPlugin("net.neoforged.gradle.userdev")) { + return null + } + + // NG userdev + def runtimes = project.extensions.findByName('userDevRuntime').runtimes.get() + def neoforgeVersion = null + for (def entry in runtimes) { + neoforgeVersion = entry.value.specification.forgeVersion + break + } + if (neoforgeVersion == null) { + return null + } + + def mappingsFile = project.tasks.neoFormMergeMappings.output.get().asFile + + def accessTransformers = extension.accessTransformers.files.asList() + + //noinspection GroovyAssignabilityCheck + return new NeoGradle7ModelImpl(neoforgeVersion, mappingsFile, accessTransformers) + } + + @Override + ErrorMessageBuilder getErrorMessageBuilder(@NotNull Project project, @NotNull Exception e) { + return ErrorMessageBuilder.create( + project, e, "MinecraftDev import errors" + ).withDescription("Unable to build MinecraftDev MCP project configuration") + } +} diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelImpl.groovy new file mode 100644 index 000000000..a94d1f347 --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neogradle/NeoGradle7ModelImpl.groovy @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mcp.gradle.tooling.neogradle + + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 +import groovy.transform.CompileStatic + +@CompileStatic +final class NeoGradle7ModelImpl implements McpModelNG7, Serializable { + + final String neoForgeVersion + final File mappingsFile + final List accessTransformers + + NeoGradle7ModelImpl( + final String neoForgeVersion, + final File mappingsFile, + final List accessTransformers + ) { + this.neoForgeVersion = neoForgeVersion + this.mappingsFile = mappingsFile + this.accessTransformers = accessTransformers + } +} diff --git a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNG7.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNG7.java new file mode 100644 index 000000000..3e50d7a85 --- /dev/null +++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNG7.java @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mcp.gradle.tooling; + +import java.io.File; +import java.util.List; + +public interface McpModelNG7 { + String getNeoForgeVersion(); + File getMappingsFile(); + List getAccessTransformers(); +} diff --git a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService index 161ce7c47..382ea8362 100644 --- a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService +++ b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService @@ -1,5 +1,6 @@ com.demonwav.mcdev.platform.mcp.gradle.tooling.archloom.ArchitecturyModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.fabricloom.FabricLoomModelBuilderImpl +com.demonwav.mcdev.platform.mcp.gradle.tooling.neogradle.NeoGradle7ModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.vanillagradle.VanillaGradleModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2BuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3BuilderImpl diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 6b0595105..815131f7f 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -84,6 +84,13 @@ class MinecraftConfigurable : Configurable { } } + group(MCDevBundle("minecraft.settings.mixin")) { + row { + checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line")) + .bindSelected(settings::isShadowAnnotationsSameLine) + } + } + onApply { for (project in ProjectManager.getInstance().openProjects) { ProjectView.getInstance(project).refresh() diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index 4df31b47d..b4b596114 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -35,6 +35,8 @@ class MinecraftSettings : PersistentStateComponent { var isShowChatColorGutterIcons: Boolean = true, var isShowChatColorUnderlines: Boolean = false, var underlineType: UnderlineType = UnderlineType.DOTTED, + + var isShadowAnnotationsSameLine: Boolean = true, ) private var state = State() @@ -78,6 +80,12 @@ class MinecraftSettings : PersistentStateComponent { state.underlineType = underlineType } + var isShadowAnnotationsSameLine: Boolean + get() = state.isShadowAnnotationsSameLine + set(shadowAnnotationsSameLine) { + state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine + } + enum class UnderlineType(private val regular: String, val effectType: EffectType) { NORMAL("Normal", EffectType.LINE_UNDERSCORE), diff --git a/src/main/kotlin/asset/PlatformAssets.kt b/src/main/kotlin/asset/PlatformAssets.kt index c06f50a5b..1f0654747 100644 --- a/src/main/kotlin/asset/PlatformAssets.kt +++ b/src/main/kotlin/asset/PlatformAssets.kt @@ -20,6 +20,8 @@ package com.demonwav.mcdev.asset +import com.intellij.util.IconUtil + @Suppress("unused") object PlatformAssets : Assets() { val MINECRAFT_ICON = loadIcon("/assets/icons/platform/Minecraft.png") @@ -62,6 +64,9 @@ object PlatformAssets : Assets() { val MIXIN_ICON_DARK = loadIcon("/assets/icons/platform/Mixins_dark.png") val MIXIN_ICON_2X_DARK = loadIcon("/assets/icons/platform/Mixins@2x_dark.png") + val NEOFORGE_ICON = IconUtil.scale(loadIcon("/assets/icons/platform/NeoForge.png"), null, 0.125f) + val NEOFORGE_ICON_2X = IconUtil.scale(loadIcon("/assets/icons/platform/NeoForge.png"), null, 0.25f) + val MCP_ICON = loadIcon("/assets/icons/platform/MCP.png") val MCP_ICON_2X = loadIcon("/assets/icons/platform/MCP@2x.png") val MCP_ICON_DARK = loadIcon("/assets/icons/platform/MCP_dark.png") diff --git a/src/main/kotlin/creator/ParchmentStep.kt b/src/main/kotlin/creator/ParchmentStep.kt new file mode 100644 index 000000000..8978477b0 --- /dev/null +++ b/src/main/kotlin/creator/ParchmentStep.kt @@ -0,0 +1,168 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.step.AbstractMcVersionChainStep +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.icons.AllIcons +import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.bindBooleanStorage +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.ui.content.AlertIcon +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import javax.swing.DefaultComboBoxModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.withContext + +class ParchmentStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep(parent) { + + val useParchmentProperty = propertyGraph.property(false) + .bindBooleanStorage("${javaClass.name}.useParchment") + var useParchment by useParchmentProperty + + val parchmentVersionProperty = propertyGraph.property(null) + var parchmentVersion by parchmentVersionProperty + + val parchmentVersionsProperty = propertyGraph.property>(emptyList()) + var parchmentVersions by parchmentVersionsProperty + + val loadingVersionsProperty = propertyGraph.property(false) + var loadingVersions by loadingVersionsProperty + + val hasParchmentVersionProperty = propertyGraph.property(false) + var hasParchmentVersion by hasParchmentVersionProperty + + val includeOlderMcVersionsProperty = propertyGraph.property(false) + .bindBooleanStorage("${javaClass.name}.includeOlderMcVersions") + var includeOlderMcVersions by includeOlderMcVersionsProperty + + val includeSnapshotsVersionsProperty = propertyGraph.property(false) + .bindBooleanStorage("${javaClass.name}.includeSnapshotsVersions") + var includeSnapshotsVersions by includeSnapshotsVersionsProperty + + private val parchmentVersionsModel = DefaultComboBoxModel(emptyArray()) + private val mcVersionProperty: ObservableMutableProperty + + init { + storeToData() + @Suppress("UNCHECKED_CAST") + mcVersionProperty = findStep().getVersionProperty(0) + as ObservableMutableProperty + mcVersionProperty.afterChange { updateVersionsModel() } + + includeOlderMcVersionsProperty.afterChange { updateVersionsModel() } + includeSnapshotsVersionsProperty.afterChange { updateVersionsModel() } + + downloadVersions() + } + + private fun updateVersionsModel() { + val mcVersion = mcVersionProperty.get() + val versions = parchmentVersions.filter { version -> + if (!includeOlderMcVersions && version.mcVersion < mcVersion) { + return@filter false + } + + if (!includeSnapshotsVersions && version.parchmentVersion.contains("-SNAPSHOT")) { + return@filter false + } + + return@filter true + } + + hasParchmentVersion = versions.isNotEmpty() + + parchmentVersionsModel.removeAllElements() + parchmentVersionsModel.addAll(versions) + + parchmentVersionsModel.selectedItem = versions.firstOrNull { !it.parchmentVersion.contains('-') } + ?: versions.firstOrNull() + + loadingVersions = false + } + + private fun downloadVersions() { + loadingVersions = true + parchmentVersionsModel.removeAllElements() + application.executeOnPooledThread { + runBlocking { + val versions = ParchmentVersion.downloadData() + withContext(Dispatchers.Swing) { + parchmentVersions = versions + updateVersionsModel() + } + } + } + } + + override fun setupUI(builder: Panel) { + with(builder) { + row(MCDevBundle("creator.ui.parchment.label")) { + checkBox("") + .bindSelected(useParchmentProperty) + + comboBox(parchmentVersionsModel) + .enabledIf(useParchmentProperty and hasParchmentVersionProperty) + .bindItem(parchmentVersionProperty) + + cell(AsyncProcessIcon("$javaClass.parchmentVersions")) + .visibleIf(loadingVersionsProperty) + + label(MCDevBundle("creator.ui.parchment.no_version.message")) + .visibleIf(hasParchmentVersionProperty.not() and loadingVersionsProperty.not()) + .applyToComponent { icon = AlertIcon(AllIcons.General.Warning) } + } + + row(MCDevBundle("creator.ui.parchment.include.label")) { + checkBox(MCDevBundle("creator.ui.parchment.include.old_mc.label")) + .enabledIf(useParchmentProperty) + .bindSelected(includeOlderMcVersionsProperty) + checkBox(MCDevBundle("creator.ui.parchment.include.snapshots.label")) + .enabledIf(useParchmentProperty) + .bindSelected(includeSnapshotsVersionsProperty) + } + } + + super.setupUI(builder) + } + + override fun setupProject(project: Project) { + data.putUserData(USE_KEY, useParchment) + data.putUserData(VERSION_KEY, parchmentVersion) + } + + companion object { + val USE_KEY = Key.create("${ParchmentStep::class.java.name}.useParchment") + val VERSION_KEY = Key.create("${ParchmentStep::class.java}.parchmentVersion") + } +} diff --git a/src/main/kotlin/creator/ParchmentVersion.kt b/src/main/kotlin/creator/ParchmentVersion.kt new file mode 100644 index 000000000..085d2f834 --- /dev/null +++ b/src/main/kotlin/creator/ParchmentVersion.kt @@ -0,0 +1,81 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.util.SemanticVersion +import com.github.kittinunf.fuel.core.FuelError +import com.github.kittinunf.fuel.core.HttpException +import com.intellij.openapi.diagnostic.logger + +class ParchmentVersion private constructor(val mcVersion: SemanticVersion, val parchmentVersion: String) { + + private val presentableString by lazy { "$mcVersion - $parchmentVersion" } + + override fun toString(): String = presentableString + + companion object { + private val LOGGER = logger() + + suspend fun downloadData(limit: Int = 50): List { + val versions = mutableListOf() + val mcVersions = collectSupportedMcVersions() ?: return emptyList() + for (mcVersion in mcVersions) { + val url = "https://maven.parchmentmc.org/org/parchmentmc/data/parchment-$mcVersion/maven-metadata.xml" + try { + collectMavenVersions(url) + .mapTo(versions) { ParchmentVersion(mcVersion, it) } + if (versions.size > limit) { + return versions.subList(0, limit) + } + } catch (e: Exception) { + if (e !is FuelError || e.exception !is HttpException) { + LOGGER.error("Failed to retrieve Parchment version data from $url", e) + } + } + } + + return versions + } + + private suspend fun collectSupportedMcVersions(): List? { + try { + val baseUrl = "https://maven.parchmentmc.org/org/parchmentmc/data/" + val scrapeArtifactoryDirectoryListing = scrapeArtifactoryDirectoryListing(baseUrl) + return scrapeArtifactoryDirectoryListing + .asReversed() + .asSequence() + .filter { it.startsWith("parchment-") } + .mapNotNull { + val mcVersion = it.removePrefix("parchment-").removeSuffix("/") + SemanticVersion.tryParse(mcVersion) + } + .toList() + } catch (e: Exception) { + if (e is FuelError && e.exception is HttpException) { + return null + } + LOGGER.error("Failed to list supported Parchment Minecraft versions", e) + } + + return null + } + } +} diff --git a/src/main/kotlin/creator/maven-repo-utils.kt b/src/main/kotlin/creator/maven-repo-utils.kt new file mode 100644 index 000000000..901a1a0af --- /dev/null +++ b/src/main/kotlin/creator/maven-repo-utils.kt @@ -0,0 +1,113 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.update.PluginUtil +import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.core.requests.suspendable +import java.io.IOException +import java.util.function.Predicate +import javax.xml.stream.XMLInputFactory +import javax.xml.stream.events.XMLEvent + +@Throws(IOException::class) +suspend fun collectMavenVersions(url: String, filter: Predicate = Predicate { true }): List { + val manager = FuelManager() + manager.proxy = selectProxy(url) + + val response = manager.get(url) + .header("User-Agent", PluginUtil.useragent) + .allowRedirects(true) + .suspendable() + .await() + + val result = mutableListOf() + response.body().toStream().use { stream -> + val inputFactory = XMLInputFactory.newInstance() + + @Suppress("UNCHECKED_CAST") + val reader = inputFactory.createXMLEventReader(stream) as Iterator + for (event in reader) { + if (!event.isStartElement) { + continue + } + val start = event.asStartElement() + val name = start.name.localPart + if (name != "version") { + continue + } + + val versionEvent = reader.next() + if (!versionEvent.isCharacters) { + continue + } + + val version = versionEvent.asCharacters().data + if (filter.test(version)) { + result += version + } + } + } + + return result +} + +@Throws(IOException::class) +suspend fun scrapeArtifactoryDirectoryListing(url: String): List { + val manager = FuelManager() + manager.proxy = selectProxy(url) + + val response = manager.get(url) + .header("User-Agent", PluginUtil.useragent) + .allowRedirects(true) + .suspendable() + .await() + + val result = mutableListOf() + response.body().toStream().use { stream -> + val inputFactory = XMLInputFactory.newInstance() + + @Suppress("UNCHECKED_CAST") + val reader = inputFactory.createXMLEventReader(stream) as Iterator + for (event in reader) { + if (!event.isStartElement) { + continue + } + val start = event.asStartElement() + val name = start.name.localPart + if (name != "a") { + continue + } + + val childPathEvent = reader.next() + if (!childPathEvent.isCharacters) { + continue + } + + val childPath = childPathEvent.asCharacters().data + if (childPath != "../") { + result += childPath + } + } + } + + return result +} diff --git a/src/main/kotlin/creator/step/McVersionStep.kt b/src/main/kotlin/creator/step/McVersionStep.kt index e52345639..7c50138d9 100644 --- a/src/main/kotlin/creator/step/McVersionStep.kt +++ b/src/main/kotlin/creator/step/McVersionStep.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.creator.step import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.JdkProjectSetupFinalizer import com.demonwav.mcdev.creator.findStep +import com.demonwav.mcdev.creator.storeToData import com.demonwav.mcdev.util.MinecraftVersions import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.onShown @@ -74,6 +75,10 @@ abstract class AbstractMcVersionChainStep( const val MINECRAFT_VERSION = 0 } + init { + storeToData() + } + override fun setupUI(builder: Panel) { super.setupUI(builder) getVersionProperty(MINECRAFT_VERSION).afterChange { diff --git a/src/main/kotlin/facet/MinecraftFacetEditorTabV2.kt b/src/main/kotlin/facet/MinecraftFacetEditorTabV2.kt index 44e549d13..60c01eae2 100644 --- a/src/main/kotlin/facet/MinecraftFacetEditorTabV2.kt +++ b/src/main/kotlin/facet/MinecraftFacetEditorTabV2.kt @@ -49,6 +49,7 @@ class MinecraftFacetEditorTabV2(private val configuration: MinecraftFacetConfigu val paper = platformState(PlatformType.PAPER) val sponge = platformState(PlatformType.SPONGE) val forge = platformState(PlatformType.FORGE) + val neoforge = platformState(PlatformType.NEOFORGE) val fabric = platformState(PlatformType.FABRIC) val architectury = platformState(PlatformType.ARCHITECTURY) val mcp = platformState(PlatformType.MCP) @@ -103,6 +104,8 @@ class MinecraftFacetEditorTabV2(private val configuration: MinecraftFacetConfigu }, ) + createRow(neoforge, "NeoForge", PlatformAssets.NEOFORGE_ICON_2X) + createRow( fabric, "Fabric", PlatformAssets.FABRIC_ICON_2X, { diff --git a/src/main/kotlin/facet/MinecraftLibraryKinds.kt b/src/main/kotlin/facet/MinecraftLibraryKinds.kt index c0f254c0a..3fcabd438 100644 --- a/src/main/kotlin/facet/MinecraftLibraryKinds.kt +++ b/src/main/kotlin/facet/MinecraftLibraryKinds.kt @@ -31,6 +31,7 @@ import com.demonwav.mcdev.platform.fabric.framework.FABRIC_LIBRARY_KIND import com.demonwav.mcdev.platform.forge.framework.FORGE_LIBRARY_KIND import com.demonwav.mcdev.platform.mcp.framework.MCP_LIBRARY_KIND import com.demonwav.mcdev.platform.mixin.framework.MIXIN_LIBRARY_KIND +import com.demonwav.mcdev.platform.neoforge.framework.NEOFORGE_LIBRARY_KIND import com.demonwav.mcdev.platform.sponge.framework.SPONGE_LIBRARY_KIND import com.demonwav.mcdev.platform.velocity.framework.VELOCITY_LIBRARY_KIND @@ -40,6 +41,7 @@ val MINECRAFT_LIBRARY_KINDS = setOf( PAPER_LIBRARY_KIND, SPONGE_LIBRARY_KIND, FORGE_LIBRARY_KIND, + NEOFORGE_LIBRARY_KIND, FABRIC_LIBRARY_KIND, ARCHITECTURY_LIBRARY_KIND, MCP_LIBRARY_KIND, diff --git a/src/main/kotlin/insight/PluginLineMarkerProvider.kt b/src/main/kotlin/insight/PluginLineMarkerProvider.kt index a48c9a854..7bd807f43 100644 --- a/src/main/kotlin/insight/PluginLineMarkerProvider.kt +++ b/src/main/kotlin/insight/PluginLineMarkerProvider.kt @@ -64,6 +64,7 @@ class PluginLineMarkerProvider : LineMarkerProviderDescriptor() { PlatformType.PAPER -> ThreeState.NO PlatformType.ARCHITECTURY -> ThreeState.YES PlatformType.FORGE -> ThreeState.YES + PlatformType.NEOFORGE -> ThreeState.YES PlatformType.FABRIC -> ThreeState.YES PlatformType.SPONGE -> ThreeState.NO PlatformType.BUNGEECORD -> ThreeState.NO diff --git a/src/main/kotlin/insight/generation/MinecraftClassCreateAction.kt b/src/main/kotlin/insight/generation/MinecraftClassCreateAction.kt index a7b049f3a..3cf11b474 100644 --- a/src/main/kotlin/insight/generation/MinecraftClassCreateAction.kt +++ b/src/main/kotlin/insight/generation/MinecraftClassCreateAction.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.fabric.FabricModuleType import com.demonwav.mcdev.platform.forge.ForgeModuleType import com.demonwav.mcdev.platform.mcp.McpModuleType +import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType import com.demonwav.mcdev.util.MinecraftTemplates import com.demonwav.mcdev.util.MinecraftVersions import com.demonwav.mcdev.util.SemanticVersion @@ -66,6 +67,7 @@ class MinecraftClassCreateAction : val module = directory.findModule() ?: return val isForge = MinecraftFacet.getInstance(module, ForgeModuleType) != null + val isNeoForge = MinecraftFacet.getInstance(module, NeoForgeModuleType) != null val isFabric = MinecraftFacet.getInstance(module, FabricModuleType) != null val mcVersion = MinecraftFacet.getInstance(module, McpModuleType)?.getSettings() ?.minecraftVersion?.let(SemanticVersion::parse) @@ -92,6 +94,16 @@ class MinecraftClassCreateAction : builder.addKind("Packet", icon, MinecraftTemplates.FORGE_1_18_PACKET_TEMPLATE) } } + + if (isNeoForge) { + val icon = PlatformAssets.NEOFORGE_ICON + builder.addKind("Block", icon, MinecraftTemplates.NEOFORGE_BLOCK_TEMPLATE) + builder.addKind("Enchantment", icon, MinecraftTemplates.NEOFORGE_ENCHANTMENT_TEMPLATE) + builder.addKind("Item", icon, MinecraftTemplates.NEOFORGE_ITEM_TEMPLATE) + builder.addKind("Mob Effect", icon, MinecraftTemplates.NEOFORGE_MOB_EFFECT_TEMPLATE) + builder.addKind("Packet", icon, MinecraftTemplates.NEOFORGE_PACKET_TEMPLATE) + } + if (isFabric) { val icon = PlatformAssets.FABRIC_ICON @@ -107,9 +119,10 @@ class MinecraftClassCreateAction : val module = psi?.findModule() ?: return false val isFabricMod = MinecraftFacet.getInstance(module, FabricModuleType) != null val isForgeMod = MinecraftFacet.getInstance(module, ForgeModuleType) != null + val isNeoForgeMod = MinecraftFacet.getInstance(module, NeoForgeModuleType) != null val hasMcVersion = MinecraftFacet.getInstance(module, McpModuleType)?.getSettings()?.minecraftVersion != null - return (isFabricMod || isForgeMod && hasMcVersion) && super.isAvailable(dataContext) + return (isFabricMod || isNeoForgeMod || isForgeMod && hasMcVersion) && super.isAvailable(dataContext) } override fun checkPackageExists(directory: PsiDirectory): Boolean { diff --git a/src/main/kotlin/platform/PlatformType.kt b/src/main/kotlin/platform/PlatformType.kt index 564866a93..a8fd5ff94 100644 --- a/src/main/kotlin/platform/PlatformType.kt +++ b/src/main/kotlin/platform/PlatformType.kt @@ -42,6 +42,8 @@ import com.demonwav.mcdev.platform.mcp.McpModuleType import com.demonwav.mcdev.platform.mcp.framework.MCP_LIBRARY_KIND import com.demonwav.mcdev.platform.mixin.MixinModuleType import com.demonwav.mcdev.platform.mixin.framework.MIXIN_LIBRARY_KIND +import com.demonwav.mcdev.platform.neoforge.NeoForgeModuleType +import com.demonwav.mcdev.platform.neoforge.framework.NEOFORGE_LIBRARY_KIND import com.demonwav.mcdev.platform.sponge.SpongeModuleType import com.demonwav.mcdev.platform.sponge.framework.SPONGE_LIBRARY_KIND import com.demonwav.mcdev.platform.velocity.VelocityModuleType @@ -65,6 +67,7 @@ enum class PlatformType( WATERFALL(WaterfallModuleType, "waterfall.json", BUNGEECORD), VELOCITY(VelocityModuleType, "velocity.json"), MIXIN(MixinModuleType), + NEOFORGE(NeoForgeModuleType), MCP(McpModuleType), ADVENTURE(AdventureModuleType), ; @@ -82,7 +85,8 @@ enum class PlatformType( companion object { fun removeParents(types: Set) = - types.filterNotNull() + types.asSequence() + .filterNotNull() .filter { type -> type.children.isEmpty() || types.none { type.children.contains(it) } } .toHashSet() @@ -100,6 +104,7 @@ enum class PlatformType( WATERFALL_LIBRARY_KIND -> WATERFALL VELOCITY_LIBRARY_KIND -> VELOCITY ADVENTURE_LIBRARY_KIND -> ADVENTURE + NEOFORGE_LIBRARY_KIND -> NEOFORGE else -> null } } diff --git a/src/main/kotlin/platform/fabric/creator/gradle-steps.kt b/src/main/kotlin/platform/fabric/creator/gradle-steps.kt index 415862b78..c3fe4e8d4 100644 --- a/src/main/kotlin/platform/fabric/creator/gradle-steps.kt +++ b/src/main/kotlin/platform/fabric/creator/gradle-steps.kt @@ -57,7 +57,7 @@ class FabricGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningA val mcVersion = data.getUserData(FabricVersionChainStep.MC_VERSION_KEY) ?: return val yarnVersion = data.getUserData(FabricVersionChainStep.YARN_VERSION_KEY) ?: return val loaderVersion = data.getUserData(FabricVersionChainStep.LOADER_VERSION_KEY) ?: return - val loomVersion = "1.4-SNAPSHOT" + val loomVersion = "1.5-SNAPSHOT" val javaVersion = findStep().preferredJdk.ordinal val apiVersion = data.getUserData(FabricVersionChainStep.API_VERSION_KEY) val officialMappings = data.getUserData(FabricVersionChainStep.OFFICIAL_MAPPINGS_KEY) ?: false diff --git a/src/main/kotlin/platform/fabric/util/fabric-util.kt b/src/main/kotlin/platform/fabric/util/fabric-util.kt new file mode 100644 index 000000000..4ce58ea4b --- /dev/null +++ b/src/main/kotlin/platform/fabric/util/fabric-util.kt @@ -0,0 +1,29 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.fabric.util + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.util.findModule +import com.intellij.psi.PsiElement + +val PsiElement.isFabric: Boolean get() = + findModule()?.let { MinecraftFacet.getInstance(it) }?.isOfType(FabricModuleType) == true diff --git a/src/main/kotlin/platform/forge/inspections/sideonly/SideAnnotation.kt b/src/main/kotlin/platform/forge/inspections/sideonly/SideAnnotation.kt index f072067ed..3b686b292 100644 --- a/src/main/kotlin/platform/forge/inspections/sideonly/SideAnnotation.kt +++ b/src/main/kotlin/platform/forge/inspections/sideonly/SideAnnotation.kt @@ -60,6 +60,12 @@ data class SideAnnotation( "CLIENT", "DEDICATED_SERVER", ), + SideAnnotation( + "net.neoforged.api.distmarker.OnlyIn", + "net.neoforged.api.distmarker.Dist", + "CLIENT", + "DEDICATED_SERVER", + ), SideAnnotation( "net.fabricmc.api.Environment", "net.fabricmc.api.EnvType", diff --git a/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt b/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt index 147937a3d..4678979d5 100644 --- a/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt +++ b/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt @@ -54,6 +54,7 @@ data class ForgePackDescriptor(val format: Int, val comment: String) { val FORMAT_12 = ForgePackDescriptor(12, "") val FORMAT_15 = ForgePackDescriptor(15, "") val FORMAT_18 = ForgePackDescriptor(18, "") + val FORMAT_26 = ForgePackDescriptor(26, "") // See https://minecraft.gamepedia.com/Tutorials/Creating_a_resource_pack#.22pack_format.22 fun forMcVersion(version: SemanticVersion): ForgePackDescriptor? = when { @@ -67,7 +68,8 @@ data class ForgePackDescriptor(val format: Int, val comment: String) { version < MinecraftVersions.MC1_19_3 -> FORMAT_10 version < MinecraftVersions.MC1_20 -> FORMAT_12 version < MinecraftVersions.MC1_20_2 -> FORMAT_15 - version >= MinecraftVersions.MC1_20_2 -> FORMAT_18 + version < MinecraftVersions.MC1_20_3 -> FORMAT_18 + version >= MinecraftVersions.MC1_20_3 -> FORMAT_26 else -> null } } diff --git a/src/main/kotlin/platform/forge/version/ForgeVersion.kt b/src/main/kotlin/platform/forge/version/ForgeVersion.kt index 46f4e5413..cfb5b1883 100644 --- a/src/main/kotlin/platform/forge/version/ForgeVersion.kt +++ b/src/main/kotlin/platform/forge/version/ForgeVersion.kt @@ -20,16 +20,11 @@ package com.demonwav.mcdev.platform.forge.version -import com.demonwav.mcdev.creator.selectProxy -import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.creator.collectMavenVersions 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 com.intellij.openapi.diagnostic.logger import java.io.IOException -import javax.xml.stream.XMLInputFactory -import javax.xml.stream.events.XMLEvent class ForgeVersion private constructor(val versions: List) { @@ -70,45 +65,8 @@ class ForgeVersion private constructor(val versions: List) { suspend fun downloadData(): ForgeVersion? { try { 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() - response.body().toStream().use { stream -> - val inputFactory = XMLInputFactory.newInstance() - - @Suppress("UNCHECKED_CAST") - val reader = inputFactory.createXMLEventReader(stream) as Iterator - for (event in reader) { - if (!event.isStartElement) { - continue - } - val start = event.asStartElement() - val name = start.name.localPart - if (name != "version") { - continue - } - - val versionEvent = reader.next() - if (!versionEvent.isCharacters) { - continue - } - val version = versionEvent.asCharacters().data - val index = version.indexOf('-') - if (index == -1) { - continue - } - - result += version - } - } - - return ForgeVersion(result) + val versions = collectMavenVersions(url) { version -> version.indexOf('-') != -1 } + return ForgeVersion(versions) } catch (e: IOException) { LOGGER.error("Failed to retrieve Forge version data", e) } diff --git a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt index 396586425..53b90142a 100644 --- a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt +++ b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt @@ -22,8 +22,10 @@ package com.demonwav.mcdev.platform.mcp.gradle import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG2Handler import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG3Handler +import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelNG7Handler import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2 import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3 +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 import com.demonwav.mcdev.util.runGradleTask import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.project.ModuleData @@ -36,7 +38,7 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { // Register our custom Gradle tooling API model in IntelliJ's project resolver override fun getExtraProjectModelClasses(): Set> = - setOf(McpModelFG2::class.java, McpModelFG3::class.java) + setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java) override fun getToolingExtensionsClasses() = extraProjectModelClasses @@ -89,6 +91,6 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { } companion object { - private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler) + private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler) } } diff --git a/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNG7Handler.kt b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNG7Handler.kt new file mode 100644 index 000000000..76292fe06 --- /dev/null +++ b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNG7Handler.kt @@ -0,0 +1,76 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mcp.gradle.datahandler + +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.at.AtFileType +import com.demonwav.mcdev.platform.mcp.gradle.McpModelData +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 +import com.demonwav.mcdev.platform.mcp.srg.SrgType +import com.demonwav.mcdev.util.runWriteTaskLater +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.fileTypes.ExactFileNameMatcher +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.vfs.LocalFileSystem +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +object McpModelNG7Handler : McpModelDataHandler { + + override fun build( + gradleModule: IdeaModule, + node: DataNode, + resolverCtx: ProjectResolverContext, + ) { + val data = resolverCtx.getExtraProject(gradleModule, McpModelNG7::class.java) ?: return + + val state = McpModuleSettings.State( + data.neoForgeVersion.substringBeforeLast('.'), + null, + data.mappingsFile.absolutePath, + SrgType.TSRG, + data.neoForgeVersion, + ) + + val ats = data.accessTransformers + if (ats != null && ats.isNotEmpty()) { + runWriteTaskLater { + for (at in ats) { + val fileTypeManager = FileTypeManager.getInstance() + val atFile = LocalFileSystem.getInstance().findFileByIoFile(at) ?: continue + fileTypeManager.associate(AtFileType, ExactFileNameMatcher(atFile.name)) + } + } + } + + val modelData = McpModelData(node.data, state, null, data.accessTransformers) + node.createChild(McpModelData.KEY, modelData) + + for (child in node.children) { + val childData = child.data + if (childData is GradleSourceSetData) { + child.createChild(McpModelData.KEY, modelData.copy(module = childData)) + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt index 7b3ec79bb..f53938550 100644 --- a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt +++ b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.action +import com.demonwav.mcdev.MinecraftSettings import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.platform.mixin.util.findFields import com.demonwav.mcdev.platform.mixin.util.findMethods @@ -236,6 +237,11 @@ private fun copyAnnotation(modifiers: PsiModifierList, newModifiers: PsiModifier } inline fun disableAnnotationWrapping(project: Project, func: () -> Unit) { + if (!MinecraftSettings.instance.isShadowAnnotationsSameLine) { + func() + return + } + val settings = CodeStyle.getSettings(project).getCommonSettings(JavaLanguage.INSTANCE) val methodWrap = settings.METHOD_ANNOTATION_WRAP val fieldWrap = settings.FIELD_ANNOTATION_WRAP diff --git a/src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt b/src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt new file mode 100644 index 000000000..0694126ec --- /dev/null +++ b/src/main/kotlin/platform/mixin/completion/AtArgsCompletionContributor.kt @@ -0,0 +1,107 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.completion + +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.insideAnnotationAttribute +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiLanguageInjectionHost +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.util.parentOfType +import com.intellij.util.ProcessingContext + +class AtArgsCompletionContributor : CompletionContributor() { + init { + for (tokenType in arrayOf(JavaTokenType.STRING_LITERAL, JavaTokenType.TEXT_BLOCK_LITERAL)) { + extend( + CompletionType.BASIC, + PsiJavaPatterns.psiElement(tokenType).withParent( + PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MixinConstants.Annotations.AT, "args") + ), + Provider, + ) + } + } + + object Provider : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val literal = parameters.position.parentOfType(withSelf = true) ?: return + val atAnnotation = literal.parentOfType() ?: return + val atCode = atAnnotation.findDeclaredAttributeValue("value")?.constantStringValue ?: return + val injectionPoint = InjectionPoint.byAtCode(atCode) ?: return + val escaper = (literal as? PsiLanguageInjectionHost)?.createLiteralTextEscaper() ?: return + val beforeCursor = buildString { + escaper.decode(TextRange(1, parameters.offset - (literal as PsiLiteral).textRange.startOffset), this) + } + val equalsIndex = beforeCursor.indexOf('=') + if (equalsIndex == -1) { + val argsKeys = injectionPoint.getArgsKeys(atAnnotation) + result.addAllElements( + argsKeys.map { completion -> + LookupElementBuilder.create(if (completion.contains('=')) completion else "$completion=") + .withPresentableText(completion) + } + ) + if (argsKeys.isNotEmpty()) { + result.stopHere() + } + } else { + val key = beforeCursor.substring(0, equalsIndex) + val argsValues = injectionPoint.getArgsValues(atAnnotation, key) + var prefix = beforeCursor.substring(equalsIndex + 1) + if (injectionPoint.isArgValueList(atAnnotation, key)) { + prefix = prefix.substringAfterLast(',').trimStart() + } + result.withPrefixMatcher(prefix).addAllElements( + argsValues.map { completion -> + when (completion) { + is LookupElement -> completion + is PsiNamedElement -> LookupElementBuilder.create(completion) + else -> LookupElementBuilder.create(completion) + } + } + ) + if (argsValues.isNotEmpty()) { + result.stopHere() + } + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 6127f4a86..d9e4eeae9 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -25,22 +25,14 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup -import com.demonwav.mcdev.platform.mixin.util.LocalVariables -import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.platform.mixin.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.toPsiType -import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantStringValue -import com.demonwav.mcdev.util.constantValue -import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule -import com.intellij.openapi.module.Module import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiType -import org.objectweb.asm.Opcodes import org.objectweb.asm.Type -import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -64,8 +56,9 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { isVarargs = true, ) - val info = ModifyVariableInfo.getModifyVariableInfo(annotation, CollectVisitor.Mode.COMPLETION) - ?: return null + val method = annotation.findContainingMethod() ?: return null + val localType = method.parameterList.getParameter(0)?.type + val info = LocalInfo.fromAnnotation(localType, annotation) val possibleTypes = mutableSetOf() for (insn in targets) { @@ -93,116 +86,3 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { return result } } - -class ModifyVariableInfo( - val type: PsiType?, - val argsOnly: Boolean, - val index: Int?, - val ordinal: Int?, - val names: Set, -) { - fun getLocals( - module: Module, - targetClass: ClassNode, - methodNode: MethodNode, - insn: AbstractInsnNode, - ): Array? { - return if (argsOnly) { - val args = mutableListOf() - if (!methodNode.hasAccess(Opcodes.ACC_STATIC)) { - val thisDesc = Type.getObjectType(targetClass.name).descriptor - args.add(LocalVariables.LocalVariable("this", thisDesc, null, null, null, 0)) - } - for (argType in Type.getArgumentTypes(methodNode.desc)) { - args.add( - LocalVariables.LocalVariable("arg${args.size}", argType.descriptor, null, null, null, args.size), - ) - if (argType.size == 2) { - args.add(null) - } - } - args.toTypedArray() - } else { - LocalVariables.getLocals(module, targetClass, methodNode, insn) - } - } - - fun matchLocals( - locals: Array, - mode: CollectVisitor.Mode, - matchType: Boolean = true, - ): List { - val typeDesc = type?.descriptor - if (ordinal != null) { - val ordinals = mutableMapOf() - val result = mutableListOf() - for (local in locals) { - if (local == null) { - continue - } - val ordinal = ordinals[local.desc] ?: 0 - ordinals[local.desc!!] = ordinal + 1 - if (ordinal == this.ordinal && (!matchType || typeDesc == null || local.desc == typeDesc)) { - result += local - } - } - return result - } - - if (index != null) { - val local = locals.firstOrNull { it?.index == index } - if (local != null) { - if (!matchType || typeDesc == null || local.desc == typeDesc) { - return listOf(local) - } - } - return emptyList() - } - - if (names.isNotEmpty()) { - val result = mutableListOf() - for (local in locals) { - if (local == null) { - continue - } - if (names.contains(local.name)) { - if (!matchType || typeDesc == null || local.desc == typeDesc) { - result += local - } - } - } - return result - } - - // implicit mode - if (mode == CollectVisitor.Mode.COMPLETION) { - return locals.asSequence() - .filterNotNull() - .filter { local -> locals.count { it?.desc == local.desc } == 1 } - .toList() - } - - return if (matchType && typeDesc != null) { - locals.singleOrNull { it?.desc == typeDesc }?.let { listOf(it) } ?: emptyList() - } else { - emptyList() - } - } - - companion object { - fun getModifyVariableInfo(modifyVariable: PsiAnnotation, mode: CollectVisitor.Mode?): ModifyVariableInfo? { - val method = modifyVariable.findContainingMethod() ?: return null - val type = method.parameterList.getParameter(0)?.type - if (type == null && mode != CollectVisitor.Mode.COMPLETION) { - return null - } - val argsOnly = modifyVariable.findDeclaredAttributeValue("argsOnly")?.constantValue as? Boolean ?: false - val index = (modifyVariable.findDeclaredAttributeValue("index")?.constantValue as? Int) - ?.takeIf { it != -1 } - val ordinal = (modifyVariable.findDeclaredAttributeValue("ordinal")?.constantValue as? Int) - ?.takeIf { it != -1 } - val names = modifyVariable.findDeclaredAttributeValue("name")?.computeStringArray()?.toSet() ?: emptySet() - return ModifyVariableInfo(type, argsOnly, index, ordinal, names) - } - } -} diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt index 931bd2f96..15b2ab3e7 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt @@ -37,11 +37,13 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiQualifiedReference import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.parentOfType +import com.intellij.psi.util.parents import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -132,6 +134,13 @@ class AtResolver( else -> constant.toString() } } + + fun findInjectorAnnotation(at: PsiAnnotation): PsiAnnotation? { + return at.parents(false) + .takeWhile { it !is PsiClass } + .filterIsInstance() + .firstOrNull { it.parent is PsiModifierList } + } } fun isUnresolved(): InsnResolutionInfo.Failure? { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt index d62d038ed..8f7078b38 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt @@ -20,13 +20,20 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.createLiteralExpression import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.enumValueOfOrNull import com.demonwav.mcdev.util.ifNotBlank +import com.demonwav.mcdev.util.mapToArray +import com.demonwav.mcdev.util.toTypedArray import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade import com.intellij.psi.JavaTokenType @@ -38,12 +45,15 @@ import com.intellij.psi.PsiClassObjectAccessExpression import com.intellij.psi.PsiElement import com.intellij.psi.PsiExpression import com.intellij.psi.PsiForeachStatement +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiSwitchLabelStatementBase import com.intellij.psi.util.PsiUtil +import com.intellij.util.ArrayUtilRt import java.util.Locale import org.objectweb.asm.Opcodes import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FrameNode import org.objectweb.asm.tree.InsnNode @@ -55,6 +65,71 @@ import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.TypeInsnNode class ConstantInjectionPoint : InjectionPoint() { + companion object { + private val ARGS_KEYS = arrayOf( + "nullValue=true", + "intValue", + "floatValue", + "longValue", + "doubleValue", + "stringValue", + "classValue", + "expandZeroConditions" + ) + } + + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + completeExtraStringAtAttribute(editor, reference, "args") + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + + override fun getArgsValues(at: PsiAnnotation, key: String): Array { + fun collectTargets(constantToCompletion: (Any) -> Any?): Array { + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + val handler = injectorAnnotation.qualifiedName + ?.let { MixinAnnotationHandler.forMixinAnnotation(it, at.project) } + ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + + val expandConditions = parseExpandConditions(AtResolver.getArgs(at)) + + return handler.resolveTarget(injectorAnnotation) + .asSequence() + .filterIsInstance() + .flatMap { target -> + target.classAndMethod.method.instructions?.let { insns -> + Iterable { insns.iterator() }.asSequence() + .mapNotNull { it.computeConstantValue(expandConditions) } + .mapNotNull(constantToCompletion) + } ?: emptySequence() + } + .toTypedArray() + } + + return when (key) { + "expandZeroConditions" -> ExpandCondition.values().mapToArray { it.name.lowercase(Locale.ROOT) } + "intValue" -> collectTargets { cst -> cst.takeIf { it is Int } } + "floatValue" -> collectTargets { cst -> cst.takeIf { it is Float } } + "longValue" -> collectTargets { cst -> cst.takeIf { it is Long } } + "doubleValue" -> collectTargets { cst -> cst.takeIf { it is Double } } + "stringValue" -> collectTargets { cst -> + (cst as? String)?.let { str -> + val escapedStr = StringUtil.escapeStringCharacters(str) + when { + str.isEmpty() -> null + escapedStr.trim() != escapedStr -> LookupElementBuilder.create(escapedStr) + .withPresentableText("\"${escapedStr}\"") + else -> escapedStr + } + } + } + "classValue" -> collectTargets { cst -> (cst as? Type)?.internalName } + else -> ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + } + + override fun isArgValueList(at: PsiAnnotation, key: String) = key == "expandZeroConditions" + fun getConstantInfo(at: PsiAnnotation): ConstantInfo? { val args = AtResolver.getArgs(at) val nullValue = args["nullValue"]?.let(java.lang.Boolean::parseBoolean) ?: false @@ -82,19 +157,15 @@ class ConstantInjectionPoint : InjectionPoint() { intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!! } - val expandConditions = args["expandZeroConditions"] + return ConstantInfo(constant, parseExpandConditions(args)) + } + + private fun parseExpandConditions(args: Map): Set { + return args["expandZeroConditions"] ?.replace(" ", "") ?.split(',') - ?.mapNotNull { - try { - ExpandCondition.valueOf(it.uppercase(Locale.ENGLISH)) - } catch (e: IllegalArgumentException) { - null - } - } + ?.mapNotNull { enumValueOfOrNull(it.uppercase(Locale.ROOT)) } ?.toSet() ?: emptySet() - - return ConstantInfo(constant, expandConditions) } private fun Boolean.toInt(): Int { @@ -242,59 +313,10 @@ class ConstantInjectionPoint : InjectionPoint() { ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) { methodNode.instructions?.iterator()?.forEachRemaining { insn -> - val constant = when (insn) { - is InsnNode -> when (insn.opcode) { - in Opcodes.ICONST_M1..Opcodes.ICONST_5 -> insn.opcode - Opcodes.ICONST_0 - Opcodes.LCONST_0 -> 0L - Opcodes.LCONST_1 -> 1L - Opcodes.FCONST_0 -> 0.0f - Opcodes.FCONST_1 -> 1.0f - Opcodes.FCONST_2 -> 2.0f - Opcodes.DCONST_0 -> 0.0 - Opcodes.DCONST_1 -> 1.0 - Opcodes.ACONST_NULL -> null - else -> return@forEachRemaining - } - - is IntInsnNode -> when (insn.opcode) { - Opcodes.BIPUSH, Opcodes.SIPUSH -> insn.operand - else -> return@forEachRemaining - } - - is LdcInsnNode -> insn.cst - is JumpInsnNode -> { - if (constantInfo == null || !constantInfo.expandConditions.any { insn.opcode in it.opcodes }) { - return@forEachRemaining - } - var lastInsn = insn.previous - while (lastInsn != null && (lastInsn is LabelNode || lastInsn is FrameNode)) { - lastInsn = lastInsn.previous - } - if (lastInsn != null) { - val lastOpcode = lastInsn.opcode - if (lastOpcode == Opcodes.LCMP || - lastOpcode == Opcodes.FCMPL || - lastOpcode == Opcodes.FCMPG || - lastOpcode == Opcodes.DCMPL || - lastOpcode == Opcodes.DCMPG - ) { - return@forEachRemaining - } - } - 0 - } - - is TypeInsnNode -> { - if (insn.opcode < Opcodes.CHECKCAST) { - // Don't treat NEW and ANEWARRAY as constants - // Matches Mixin's handling - return@forEachRemaining - } - Type.getObjectType(insn.desc) - } - - else -> return@forEachRemaining - } + val constant = ( + insn.computeConstantValue(constantInfo?.expandConditions ?: emptySet()) + ?: return@forEachRemaining + ).let { if (it is NullSentinel) null else it } if (constantInfo != null && constant != constantInfo.constant) { return@forEachRemaining @@ -338,3 +360,61 @@ class ConstantInjectionPoint : InjectionPoint() { } } } + +private object NullSentinel + +private fun AbstractInsnNode.computeConstantValue(expandConditions: Set): Any? { + return when (this) { + is InsnNode -> when (opcode) { + in Opcodes.ICONST_M1..Opcodes.ICONST_5 -> opcode - Opcodes.ICONST_0 + Opcodes.LCONST_0 -> 0L + Opcodes.LCONST_1 -> 1L + Opcodes.FCONST_0 -> 0.0f + Opcodes.FCONST_1 -> 1.0f + Opcodes.FCONST_2 -> 2.0f + Opcodes.DCONST_0 -> 0.0 + Opcodes.DCONST_1 -> 1.0 + Opcodes.ACONST_NULL -> NullSentinel + else -> null + } + + is IntInsnNode -> when (opcode) { + Opcodes.BIPUSH, Opcodes.SIPUSH -> operand + else -> null + } + + is LdcInsnNode -> cst + is JumpInsnNode -> { + if (expandConditions.none { opcode in it.opcodes }) { + return null + } + var lastInsn = previous + while (lastInsn != null && (lastInsn is LabelNode || lastInsn is FrameNode)) { + lastInsn = lastInsn.previous + } + if (lastInsn != null) { + val lastOpcode = lastInsn.opcode + if (lastOpcode == Opcodes.LCMP || + lastOpcode == Opcodes.FCMPL || + lastOpcode == Opcodes.FCMPG || + lastOpcode == Opcodes.DCMPL || + lastOpcode == Opcodes.DCMPG + ) { + return null + } + } + 0 + } + + is TypeInsnNode -> { + if (opcode < Opcodes.CHECKCAST) { + // Don't treat NEW and ANEWARRAY as constants + // Matches Mixin's handling + return null + } + Type.getObjectType(desc) + } + + else -> null + } +} diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt index 235c4b5f3..f2a0a48ba 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt @@ -20,24 +20,117 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.createLiteralExpression +import com.demonwav.mcdev.util.toTypedArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.parentOfType +import com.intellij.util.ArrayUtilRt import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() { + companion object { + private val ARGS_KEYS = arrayOf("ldc") + } + + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + val at = reference.parentOfType() ?: return + var setCursorTo: String? = null + + if (at.findDeclaredAttributeValue("target") == null) { + at.setDeclaredAttributeValue( + "target", + JavaPsiFacade.getElementFactory(reference.project).createLiteralExpression(""), + ) + setCursorTo = "target" + } + + if (at.findDeclaredAttributeValue("args") == null) { + at.setDeclaredAttributeValue( + "args", + JavaPsiFacade.getElementFactory(reference.project).createLiteralExpression("ldc="), + ) + if (setCursorTo == null) { + setCursorTo = "args" + } + } + + if (setCursorTo == null) { + return + } + + CodeStyleManager.getInstance(reference.project).reformat(at.parameterList) + + val cursorElement = at.findDeclaredAttributeValue(setCursorTo) ?: return + editor.caretModel.moveToOffset(cursorElement.textRange.endOffset - 1) + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + + override fun getArgsValues(at: PsiAnnotation, key: String): Array { + if (key != "ldc") { + return ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + val handler = injectorAnnotation.qualifiedName + ?.let { MixinAnnotationHandler.forMixinAnnotation(it, at.project) } + ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + + return handler.resolveTarget(injectorAnnotation).asSequence() + .filterIsInstance() + .flatMap { target -> + val insns = target.classAndMethod.method.instructions ?: return@flatMap emptySequence() + var lastSeenString: String? = null + val completionOptions = mutableSetOf() + insns.iterator().forEachRemaining { insn -> + val lastSeenStr = lastSeenString + if (lastSeenStr != null && insn is MethodInsnNode && insn.desc == "(Ljava/lang/String;)V") { + if (lastSeenStr.isNotEmpty()) { + val escaped = StringUtil.escapeStringCharacters(lastSeenStr) + completionOptions += if (escaped.trim() != escaped) { + LookupElementBuilder.create(escaped) + .withPresentableText("\"$escaped\"") + } else { + escaped + } + } + } + + val cst = (insn as? LdcInsnNode)?.cst + if (cst is String) { + lastSeenString = cst + } else if (insn.opcode != -1) { + lastSeenString = null + } + } + + completionOptions.asSequence() + } + .toTypedArray() + } + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt new file mode 100644 index 000000000..a4f608958 --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/CtorHeadInjectionPoint.kt @@ -0,0 +1,218 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint + +import com.demonwav.mcdev.platform.fabric.util.isFabric +import com.demonwav.mcdev.platform.mixin.inspection.injector.CtorHeadNoUnsafeInspection +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod +import com.demonwav.mcdev.platform.mixin.util.findSuperConstructorCall +import com.demonwav.mcdev.platform.mixin.util.isConstructor +import com.demonwav.mcdev.util.createLiteralExpression +import com.demonwav.mcdev.util.enumValueOfOrNull +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findInspection +import com.demonwav.mcdev.util.mapToArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiField +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiMethodReferenceExpression +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiStatement +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.parentOfType +import com.intellij.util.ArrayUtilRt +import com.intellij.util.JavaPsiConstructorUtil +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodNode + +class CtorHeadInjectionPoint : InjectionPoint() { + companion object { + private val ARGS_KEYS = arrayOf("enforce") + } + + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + val project = reference.project + + // avoid adding unsafe = true when it's unnecessary on Fabric + val noUnsafeInspection = + project.findInspection(CtorHeadNoUnsafeInspection.SHORT_NAME) + if (reference.isFabric && noUnsafeInspection?.ignoreForFabric == true) { + return + } + + val at = reference.parentOfType() ?: return + at.setDeclaredAttributeValue( + "unsafe", + JavaPsiFacade.getElementFactory(project).createLiteralExpression(true) + ) + CodeStyleManager.getInstance(project).reformat(at) + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + override fun getArgsValues(at: PsiAnnotation, key: String): Array = if (key == "enforce") { + EnforceMode.values().mapToArray { it.name } + } else { + ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + + override fun createNavigationVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: PsiClass + ): NavigationVisitor { + val args = AtResolver.getArgs(at) + val enforce = args["enforce"]?.let { enumValueOfOrNull(it) } ?: EnforceMode.DEFAULT + return MyNavigationVisitor(enforce) + } + + override fun doCreateCollectVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: ClassNode, + mode: CollectVisitor.Mode + ): CollectVisitor { + val args = AtResolver.getArgs(at) + val enforce = args["enforce"]?.let { enumValueOfOrNull(it) } ?: EnforceMode.DEFAULT + return MyCollectVisitor(at.project, targetClass, mode, enforce) + } + + override fun createLookup( + targetClass: ClassNode, + result: CollectVisitor.Result + ): LookupElementBuilder? { + return null + } + + enum class EnforceMode { + DEFAULT, POST_DELEGATE, POST_INIT + } + + private class MyCollectVisitor( + project: Project, + clazz: ClassNode, + mode: Mode, + private val enforce: EnforceMode, + ) : HeadInjectionPoint.MyCollectVisitor(project, clazz, mode) { + override fun accept(methodNode: MethodNode) { + val insns = methodNode.instructions ?: return + + if (!methodNode.isConstructor) { + super.accept(methodNode) + return + } + + val superCtorCall = methodNode.findSuperConstructorCall() ?: run { + super.accept(methodNode) + return + } + + if (enforce == EnforceMode.POST_DELEGATE) { + val insn = superCtorCall.next ?: return + addResult(insn, methodNode.findOrConstructSourceMethod(clazz, project)) + return + } + + // Although Mumfrey's original intention was to target the last *unique* field store, + // i.e. ignore duplicate field stores that occur later, due to a bug in the implementation + // it simply finds the last PUTFIELD whose owner is the target class. Mumfrey now says he + // doesn't want to change the implementation in case of breaking mixins that rely on this + // behavior, so it is now effectively intended, so it's what we'll use here. + val lastFieldStore = generateSequence(insns.last) { it.previous } + .takeWhile { it !== superCtorCall } + .firstOrNull { insn -> + insn.opcode == Opcodes.PUTFIELD && + (insn as FieldInsnNode).owner == clazz.name + } ?: superCtorCall + + val lastFieldStoreNext = lastFieldStore.next ?: return + addResult(lastFieldStoreNext, methodNode.findOrConstructSourceMethod(clazz, project)) + } + } + + private class MyNavigationVisitor(private val enforce: EnforceMode) : NavigationVisitor() { + private var isConstructor = true + private var firstStatement = true + private lateinit var elementToReturn: PsiElement + + override fun visitStart(executableElement: PsiElement) { + isConstructor = executableElement is PsiMethod && executableElement.isConstructor + elementToReturn = executableElement + } + + override fun visitExpression(expression: PsiExpression) { + if (firstStatement) { + elementToReturn = expression + firstStatement = false + } + super.visitExpression(expression) + } + + override fun visitStatement(statement: PsiStatement) { + if (firstStatement) { + elementToReturn = statement + firstStatement = false + } + super.visitStatement(statement) + } + + override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { + super.visitMethodCallExpression(expression) + if (isConstructor) { + if (JavaPsiConstructorUtil.isChainedConstructorCall(expression) || + JavaPsiConstructorUtil.isSuperConstructorCall(expression) + ) { + elementToReturn = expression + } + } + } + + override fun visitReferenceExpression(expression: PsiReferenceExpression) { + super.visitReferenceExpression(expression) + if (isConstructor && + enforce != EnforceMode.POST_DELEGATE && + expression !is PsiMethodReferenceExpression && + PsiUtil.isAccessedForWriting(expression) + ) { + val resolvedField = expression.resolve() + if (resolvedField is PsiField && resolvedField.containingClass == expression.findContainingClass()) { + elementToReturn = expression + } + } + } + + override fun visitEnd(executableElement: PsiElement) { + addResult(elementToReturn) + } + } +} diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt index 53b7ac9a4..10cada293 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt @@ -29,15 +29,18 @@ import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.getQualifiedMemberReference import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiArrayAccessExpression import com.intellij.psi.PsiClass import com.intellij.psi.PsiField +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiModifier import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.util.PsiUtil +import com.intellij.util.ArrayUtilRt import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -48,8 +51,19 @@ import org.objectweb.asm.tree.MethodNode class FieldInjectionPoint : QualifiedInjectionPoint() { companion object { private val VALID_OPCODES = setOf(Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.PUTFIELD, Opcodes.PUTSTATIC) + private val ARGS_KEYS = arrayOf("array") + private val ARRAY_VALUES = arrayOf("length", "get", "set") } + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + completeExtraStringAtAttribute(editor, reference, "target") + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + + override fun getArgsValues(at: PsiAnnotation, key: String): Array = + ARRAY_VALUES.takeIf { key == "array" } ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY + private fun getArrayAccessType(args: Map): ArrayAccessType? { return when (args["array"]) { "length" -> ArrayAccessType.LENGTH diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt index 3d1743f49..c08890dfb 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/HeadInjectionPoint.kt @@ -57,9 +57,9 @@ class HeadInjectionPoint : InjectionPoint() { return null } - private class MyCollectVisitor( - private val project: Project, - private val clazz: ClassNode, + internal open class MyCollectVisitor( + protected val project: Project, + protected val clazz: ClassNode, mode: Mode, ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index 6eb82e743..8939f36ae 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.createLiteralExpression import com.demonwav.mcdev.util.equivalentTo import com.demonwav.mcdev.util.findAnnotations import com.demonwav.mcdev.util.fullQualifiedName @@ -36,6 +37,7 @@ import com.demonwav.mcdev.util.realName import com.demonwav.mcdev.util.shortName import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor import com.intellij.openapi.extensions.RequiredElement import com.intellij.openapi.project.Project import com.intellij.openapi.util.KeyedExtensionCollector @@ -48,14 +50,17 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiExpression import com.intellij.psi.PsiLambdaExpression +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiSubstitutor +import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.parentOfType import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.ArrayUtilRt import com.intellij.util.KeyedLazyInstance import com.intellij.util.xmlb.annotations.Attribute import org.objectweb.asm.tree.AbstractInsnNode @@ -76,6 +81,33 @@ abstract class InjectionPoint { open fun usesMemberReference() = false + open fun onCompleted(editor: Editor, reference: PsiLiteral) { + } + + protected fun completeExtraStringAtAttribute(editor: Editor, reference: PsiLiteral, attributeName: String) { + val at = reference.parentOfType() ?: return + if (at.findDeclaredAttributeValue(attributeName) != null) { + return + } + at.setDeclaredAttributeValue( + attributeName, + JavaPsiFacade.getElementFactory(reference.project).createLiteralExpression("") + ) + val formattedAt = CodeStyleManager.getInstance(reference.project).reformat(at) as PsiAnnotation + val targetElement = formattedAt.findDeclaredAttributeValue(attributeName) ?: return + editor.caretModel.moveToOffset(targetElement.textRange.startOffset + 1) + } + + open fun getArgsKeys(at: PsiAnnotation): Array { + return ArrayUtilRt.EMPTY_STRING_ARRAY + } + + open fun getArgsValues(at: PsiAnnotation, key: String): Array { + return ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + + open fun isArgValueList(at: PsiAnnotation, key: String) = false + abstract fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, @@ -289,6 +321,9 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { result += element } + open fun visitStart(executableElement: PsiElement) { + } + open fun visitEnd(executableElement: PsiElement) { } @@ -299,6 +334,7 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { override fun visitMethod(method: PsiMethod) { if (!hasVisitedAnything) { + visitStart(method) super.visitMethod(method) visitEnd(method) } @@ -307,6 +343,7 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { override fun visitAnonymousClass(aClass: PsiAnonymousClass) { // do not recurse into anonymous classes if (!hasVisitedAnything) { + visitStart(aClass) super.visitAnonymousClass(aClass) visitEnd(aClass) } @@ -315,6 +352,7 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { override fun visitClass(aClass: PsiClass) { // do not recurse into inner classes if (!hasVisitedAnything) { + visitStart(aClass) super.visitClass(aClass) visitEnd(aClass) } @@ -322,6 +360,9 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) { val hadVisitedAnything = hasVisitedAnything + if (!hadVisitedAnything) { + visitStart(expression) + } super.visitMethodReferenceExpression(expression) if (!hadVisitedAnything) { visitEnd(expression) @@ -331,6 +372,7 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { override fun visitLambdaExpression(expression: PsiLambdaExpression) { // do not recurse into lambda expressions if (!hasVisitedAnything) { + visitStart(expression) super.visitLambdaExpression(expression) visitEnd(expression) } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt index b263046e8..6448dfca3 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.util.MemberReference +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade @@ -30,6 +31,7 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiForeachStatement +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiNewExpression @@ -38,6 +40,10 @@ import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : AbstractMethodInjectionPoint() { + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + completeExtraStringAtAttribute(editor, reference, "target") + } + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index 5fd43dcbe..4e7378045 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt @@ -20,12 +20,13 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint -import com.demonwav.mcdev.platform.mixin.handlers.ModifyVariableInfo import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil +import com.demonwav.mcdev.platform.mixin.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.LocalVariables import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_VARIABLE import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.isErasureEquivalentTo import com.intellij.codeInsight.lookup.LookupElementBuilder @@ -53,12 +54,18 @@ import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.VarInsnNode abstract class AbstractLoadInjectionPoint(private val store: Boolean) : InjectionPoint() { - private fun getModifyVariableInfo(at: PsiAnnotation, mode: CollectVisitor.Mode?): ModifyVariableInfo? { + private fun getModifyVariableInfo(at: PsiAnnotation, mode: CollectVisitor.Mode?): LocalInfo? { val modifyVariable = at.parentOfType() ?: return null if (!modifyVariable.hasQualifiedName(MODIFY_VARIABLE)) { return null } - return ModifyVariableInfo.getModifyVariableInfo(modifyVariable, mode) + + val method = modifyVariable.findContainingMethod() ?: return null + val localType = method.parameterList.getParameter(0)?.type + if (localType == null && mode != CollectVisitor.Mode.COMPLETION) { + return null + } + return LocalInfo.fromAnnotation(localType, modifyVariable) } override fun createNavigationVisitor( @@ -115,7 +122,7 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio } private class MyNavigationVisitor( - private val info: ModifyVariableInfo, + private val info: LocalInfo, private val store: Boolean, ) : NavigationVisitor() { override fun visitThisExpression(expression: PsiThisExpression) { @@ -276,7 +283,7 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio private val module: Module, private val targetClass: ClassNode, mode: Mode, - private val info: ModifyVariableInfo, + private val info: LocalInfo, private val store: Boolean, ) : CollectVisitor(mode) { override fun accept(methodNode: MethodNode) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index 63e81afe2..39ae4ba10 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -20,8 +20,10 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.reference.MixinSelectorParser +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass import com.demonwav.mcdev.platform.mixin.util.findMethod @@ -32,18 +34,22 @@ import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.internalName import com.demonwav.mcdev.util.shortName +import com.demonwav.mcdev.util.toTypedArray import com.intellij.codeInsight.completion.JavaLookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiNewExpression import com.intellij.psi.PsiSubstitutor import com.intellij.psi.util.parentOfType +import com.intellij.util.ArrayUtilRt import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -52,6 +58,32 @@ import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.TypeInsnNode class NewInsnInjectionPoint : InjectionPoint() { + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + completeExtraStringAtAttribute(editor, reference, "target") + } + + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS + + override fun getArgsValues(at: PsiAnnotation, key: String): Array { + if (key != "class") { + return ArrayUtilRt.EMPTY_OBJECT_ARRAY + } + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + val handler = injectorAnnotation.qualifiedName + ?.let { MixinAnnotationHandler.forMixinAnnotation(it, at.project) } + ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY + + return handler.resolveTarget(injectorAnnotation).asSequence() + .filterIsInstance() + .flatMap { target -> + target.classAndMethod.method.instructions?.asSequence()?.mapNotNull { insn -> + (insn as? TypeInsnNode)?.desc?.takeIf { insn.opcode == Opcodes.NEW } + } ?: emptySequence() + } + .toTypedArray() + } + private fun getTarget(at: PsiAnnotation, target: MixinSelector?): MixinSelector? { if (target != null) { return target @@ -151,6 +183,8 @@ class NewInsnInjectionPoint : InjectionPoint() { } companion object { + private val ARGS_KEYS = arrayOf("class") + fun findInitCall(newInsn: TypeInsnNode): MethodInsnNode? { var newInsns = 0 var insn: AbstractInsnNode? = newInsn diff --git a/src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt b/src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt new file mode 100644 index 000000000..2f191cfb1 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/fix/AnnotationAttributeFix.kt @@ -0,0 +1,112 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.fix + +import com.demonwav.mcdev.util.createLiteralExpression +import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiAnnotationMemberValue +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleManager + +open class AnnotationAttributeFix( + annotation: PsiAnnotation, + vararg attributes: Pair, +) : LocalQuickFixOnPsiElement(annotation) { + @SafeFieldForPreview + private val attributes = attributes.map { (key, value) -> + key to value?.let { + if (it !is PsiAnnotationMemberValue) { + JavaPsiFacade.getElementFactory(annotation.project).createLiteralExpression(it) + } else { + it + } + } + } + + private val description = run { + val added = this.attributes + .filter { (_, value) -> value != null } + .map { (key, value) -> "$key = ${value!!.text}" } + val removed = this.attributes + .filter { (_, value) -> value == null } + .map { (key, _) -> key } + + buildString { + if (added.isNotEmpty()) { + append("Add ") + for ((i, add) in added.withIndex()) { + if (i != 0) { + if (i == added.size - 1) { + append(" and ") + } else { + append(", ") + } + } + append(add) + } + if (removed.isNotEmpty()) { + append(", and remove ") + } + } else if (removed.isNotEmpty()) { + append("Remove ") + } + if (removed.isNotEmpty()) { + for ((i, rem) in removed.withIndex()) { + if (i != 0) { + if (i == removed.size - 1) { + append(" and ") + } else { + append(", ") + } + } + append(rem) + } + } + } + } + + override fun getFamilyName() = description + override fun getText() = description + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val annotation = startElement as? PsiAnnotation ?: return + for ((key, value) in attributes) { + annotation.setDeclaredAttributeValue(key, value) + } + + // replace single "value = foo" with "foo" + val attrs = annotation.parameterList.attributes + if (attrs.size == 1 && attrs[0].name == "value") { + attrs[0].value?.let { value -> + val fakeAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText("@Foo(0)", null) + fakeAnnotation.parameterList.attributes[0].value!!.replace(value) + annotation.parameterList.replace(fakeAnnotation.parameterList) + } + } + + CodeStyleManager.getInstance(project).reformat(annotation.parameterList) + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadNoUnsafeInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadNoUnsafeInspection.kt new file mode 100644 index 000000000..f3156c7d8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadNoUnsafeInspection.kt @@ -0,0 +1,86 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.fabric.util.isFabric +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.constantValue +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor +import java.awt.FlowLayout +import javax.swing.JCheckBox +import javax.swing.JComponent +import javax.swing.JPanel + +class CtorHeadNoUnsafeInspection : MixinInspection() { + @JvmField + var ignoreForFabric = true + + override fun createOptionsPanel(): JComponent { + val panel = JPanel(FlowLayout(FlowLayout.LEFT)) + val checkbox = JCheckBox("Ignore in Fabric mods", ignoreForFabric) + checkbox.addActionListener { + ignoreForFabric = checkbox.isSelected + } + panel.add(checkbox) + return panel + } + + override fun getStaticDescription() = "Reports when @At(\"CTOR_HEAD\") is missing unsafe" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { + if (ignoreForFabric) { + val isFabric = holder.file.isFabric + if (isFabric) { + return PsiElementVisitor.EMPTY_VISITOR + } + } + + return object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + val valueElement = annotation.findDeclaredAttributeValue("value") + if (valueElement?.constantStringValue != "CTOR_HEAD") { + return + } + if (annotation.findDeclaredAttributeValue("unsafe")?.constantValue == true) { + return + } + holder.registerProblem( + valueElement, + "CTOR_HEAD is missing unsafe = true", + AnnotationAttributeFix(annotation, "unsafe" to true), + ) + } + } + } + + companion object { + const val SHORT_NAME = "CtorHeadNoUnsafe" + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadPostInitInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadPostInitInspection.kt new file mode 100644 index 000000000..9955c51b6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadPostInitInspection.kt @@ -0,0 +1,80 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CtorHeadInjectionPoint +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.isConstructor +import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.enumValueOfOrNull +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor +import org.objectweb.asm.Opcodes + +class CtorHeadPostInitInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when CTOR_HEAD with enforce=POST_INIT doesn't target a field" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + if (annotation.findDeclaredAttributeValue("value")?.constantValue != "CTOR_HEAD") { + return + } + + val atArgs = annotation.findDeclaredAttributeValue("args") ?: return + val enforce = AtResolver.getArgs(annotation)["enforce"] + ?.let { enumValueOfOrNull(it) } + ?: CtorHeadInjectionPoint.EnforceMode.DEFAULT + if (enforce != CtorHeadInjectionPoint.EnforceMode.POST_INIT) { + return + } + + val injectorAnnotation = AtResolver.findInjectorAnnotation(annotation) ?: return + val handler = injectorAnnotation.qualifiedName + ?.let { MixinAnnotationHandler.forMixinAnnotation(it, holder.project) } + ?: return + val targets = handler.resolveTarget(injectorAnnotation).filterIsInstance() + + if (targets.any { + it.classAndMethod.method.isConstructor && + AtResolver(annotation, it.classAndMethod.clazz, it.classAndMethod.method) + .resolveInstructions() + .any { insn -> insn.insn.previous?.opcode != Opcodes.PUTFIELD } + } + ) { + holder.registerProblem( + atArgs, + "CTOR_HEAD with enforce=POST_INIT doesn't target a field", + AnnotationAttributeFix(annotation, "args" to null) + ) + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadUsedForNonConstructorInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadUsedForNonConstructorInspection.kt new file mode 100644 index 000000000..95c90327a --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/CtorHeadUsedForNonConstructorInspection.kt @@ -0,0 +1,59 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantValue +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor + +class CtorHeadUsedForNonConstructorInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when CTOR_HEAD is used without targeting a constructor method" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + val atValue = annotation.findDeclaredAttributeValue("value") ?: return + if (atValue.constantValue != "CTOR_HEAD") { + return + } + if (!UnnecessaryUnsafeInspection.mightTargetConstructor(holder.project, annotation)) { + holder.registerProblem( + atValue, + "CTOR_HEAD used without targeting a constructor", + ReplaceWithHeadFix(annotation), + ) + } + } + } + + private class ReplaceWithHeadFix(at: PsiAnnotation) : + AnnotationAttributeFix(at, "value" to "HEAD", "unsafe" to null) { + override fun getFamilyName() = "Replace with \"HEAD\"" + override fun getText() = "Replace with \"HEAD\"" + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InjectIntoConstructorInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InjectIntoConstructorInspection.kt index 46f8a47cf..578b9076d 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InjectIntoConstructorInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InjectIntoConstructorInspection.kt @@ -20,18 +20,22 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector -import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.platform.fabric.util.isFabric import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.INJECT +import com.demonwav.mcdev.platform.mixin.util.findSuperConstructorCall import com.demonwav.mcdev.platform.mixin.util.isConstructor +import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.findAnnotation -import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findAnnotations import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiClass import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiMethod import java.awt.FlowLayout @@ -46,7 +50,7 @@ class InjectIntoConstructorInspection : MixinInspection() { override fun createOptionsPanel(): JComponent { val panel = JPanel(FlowLayout(FlowLayout.LEFT)) - val checkbox = JCheckBox("Allow @Inject into constructors in Fabric", allowOnFabric) + val checkbox = JCheckBox("Always allow @Inject into constructors in Fabric", allowOnFabric) checkbox.addActionListener { allowOnFabric = checkbox.isSelected } @@ -55,31 +59,61 @@ class InjectIntoConstructorInspection : MixinInspection() { } override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { - val isFabric = holder.file.findModule()?.let { MinecraftFacet.getInstance(it) }?.isOfType(FabricModuleType) - ?: false - if (isFabric && allowOnFabric) { - return PsiElementVisitor.EMPTY_VISITOR - } - + val isFabric = holder.file.isFabric return object : JavaElementVisitor() { override fun visitMethod(method: PsiMethod) { - super.visitMethod(method) val injectAnnotation = method.findAnnotation(INJECT) ?: return val problemElement = injectAnnotation.nameReferenceElement ?: return val handler = MixinAnnotationHandler.forMixinAnnotation(INJECT) as? InjectorAnnotationHandler ?: return val targets = handler.resolveTarget(injectAnnotation) + + val ats = injectAnnotation.findDeclaredAttributeValue("at") + ?.findAnnotations() + ?: emptyList() + for (target in targets) { if (target !is MethodTargetMember || !target.classAndMethod.method.isConstructor) { continue } val (targetClass, targetMethod) = target.classAndMethod - val instructions = handler.resolveInstructions(injectAnnotation, targetClass, targetMethod) - if (instructions.any { it.insn.opcode != Opcodes.RETURN }) { - holder.registerProblem( - problemElement, - "Cannot inject into constructors at non-return instructions", - ) - return + + for (at in ats) { + val isUnsafe = at.findDeclaredAttributeValue("unsafe")?.constantValue as? Boolean + ?: (isFabric && allowOnFabric) + + val instructions = AtResolver(at, targetClass, targetMethod).resolveInstructions() + if (!isUnsafe && instructions.any { it.insn.opcode != Opcodes.RETURN }) { + val atClass = at.nameReferenceElement?.resolve() as? PsiClass + val atHasUnsafe = !atClass?.findMethodsByName("unsafe", false).isNullOrEmpty() + + val quickFixes = if (atHasUnsafe) { + arrayOf(AnnotationAttributeFix(at, "unsafe" to true)) + } else { + emptyArray() + } + + holder.registerProblem( + problemElement, + "Cannot inject into constructors at non-return instructions", + *quickFixes, + ) + return + } + + val superCtorCall = targetMethod.findSuperConstructorCall() + if (superCtorCall != null && + instructions.any { + val insnIndex = targetMethod.instructions.indexOf(it.insn) + val superCtorIndex = targetMethod.instructions.indexOf(superCtorCall) + insnIndex <= superCtorIndex + } + ) { + holder.registerProblem( + problemElement, + "Cannot inject before super() call", + ) + return + } } } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 721ceac44..5d636de7f 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -112,6 +112,18 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { false, ), ) + } else if (!shouldBeStatic && modifiers.hasModifierProperty(PsiModifier.STATIC)) { + reportedStatic = true + holder.registerProblem( + identifier, + "Method must not be static", + QuickFixFactory.getInstance().createModifierListFix( + modifiers, + PsiModifier.STATIC, + false, + false, + ), + ) } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt index 9029dac65..0d7446354 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/ModifyVariableArgsOnlyInspection.kt @@ -22,43 +22,33 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.ClassAndMethodNode import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_VARIABLE import com.demonwav.mcdev.platform.mixin.util.hasAccess import com.demonwav.mcdev.util.constantValue -import com.demonwav.mcdev.util.createLiteralExpression import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.findAnnotation import com.demonwav.mcdev.util.ifEmpty -import com.intellij.codeInspection.LocalQuickFixOnPsiElement import com.intellij.codeInspection.ProblemsHolder -import com.intellij.openapi.project.Project import com.intellij.psi.JavaElementVisitor -import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation -import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiType import org.objectweb.asm.Opcodes import org.objectweb.asm.Type class ModifyVariableArgsOnlyInspection : MixinInspection() { + override fun getStaticDescription() = + "Checks that @ModifyVariable has argsOnly if it targets arguments, which improves performance of the mixin" + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { return object : JavaElementVisitor() { override fun visitMethod(method: PsiMethod) { val modifyVariable = method.findAnnotation(MODIFY_VARIABLE) ?: return - if (modifyVariable.findDeclaredAttributeValue("argsOnly")?.constantValue == true) { - return - } - val ordinal = (modifyVariable.findDeclaredAttributeValue("ordinal")?.constantValue as? Int?) - ?.takeIf { it != -1 } - val index = (modifyVariable.findDeclaredAttributeValue("index")?.constantValue as? Int?) - ?.takeIf { it != -1 } - if (ordinal == null && index == null && modifyVariable.findDeclaredAttributeValue("name") != null) { - return - } - val wantedType = method.parameterList.getParameter(0)?.type?.descriptor ?: return + val wantedType = method.parameterList.getParameter(0)?.type ?: return val problemElement = modifyVariable.nameReferenceElement ?: return val handler = MixinAnnotationHandler.forMixinAnnotation(MODIFY_VARIABLE) ?: return @@ -66,50 +56,67 @@ class ModifyVariableArgsOnlyInspection : MixinInspection() { val methodTargets = targets.asSequence() .filterIsInstance() .map { it.classAndMethod } - for ((targetClass, targetMethod) in methodTargets) { - val argTypes = mutableListOf() - if (!targetMethod.hasAccess(Opcodes.ACC_STATIC)) { - argTypes += "L${targetClass.name};" - } - for (arg in Type.getArgumentTypes(targetMethod.desc)) { - argTypes += arg.descriptor - if (arg.size == 2) { - argTypes += null - } - } - if (ordinal != null) { - if (argTypes.asSequence().filter { it == wantedType }.count() <= ordinal) { - return - } - } else if (index != null) { - if (argTypes.size <= index) { - return - } - } else { - if (argTypes.asSequence().filter { it == wantedType }.count() != 1) { - return - } - } + if (shouldReport(modifyVariable, wantedType, methodTargets)) { + val description = "@ModifyVariable may be argsOnly = true" + holder.registerProblem( + problemElement, + description, + AnnotationAttributeFix(modifyVariable, "argsOnly" to true), + ) } - - val description = "ModifyVariable may be argsOnly = true" - holder.registerProblem(problemElement, description, AddArgsOnlyFix(modifyVariable)) } } } - override fun getStaticDescription() = - "Checks that ModifyVariable has argsOnly if it targets arguments, which improves performance of the mixin" + companion object { + fun shouldReport( + annotation: PsiAnnotation, + wantedType: PsiType, + methodTargets: Sequence, + ): Boolean { + if (annotation.findDeclaredAttributeValue("argsOnly")?.constantValue == true) { + return false + } + + val ordinal = (annotation.findDeclaredAttributeValue("ordinal")?.constantValue as? Int?) + ?.takeIf { it != -1 } + val index = (annotation.findDeclaredAttributeValue("index")?.constantValue as? Int?) + ?.takeIf { it != -1 } + if (ordinal == null && index == null && annotation.findDeclaredAttributeValue("name") != null) { + return false + } + + val wantedDesc = wantedType.descriptor + + for ((targetClass, targetMethod) in methodTargets) { + val argTypes = mutableListOf() + if (!targetMethod.hasAccess(Opcodes.ACC_STATIC)) { + argTypes += "L${targetClass.name};" + } + for (arg in Type.getArgumentTypes(targetMethod.desc)) { + argTypes += arg.descriptor + if (arg.size == 2) { + argTypes += null + } + } - private class AddArgsOnlyFix(annotation: PsiAnnotation) : LocalQuickFixOnPsiElement(annotation) { - override fun getFamilyName() = "Add argsOnly = true" - override fun getText() = "Add argsOnly = true" + if (ordinal != null) { + if (argTypes.asSequence().filter { it == wantedDesc }.count() <= ordinal) { + return false + } + } else if (index != null) { + if (argTypes.size <= index) { + return false + } + } else { + if (argTypes.asSequence().filter { it == wantedDesc }.count() != 1) { + return false + } + } + } - override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val annotation = startElement as? PsiAnnotation ?: return - val trueExpr = JavaPsiFacade.getElementFactory(project).createLiteralExpression(true) - annotation.setDeclaredAttributeValue("argsOnly", trueExpr) + return true } } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/UnnecessaryUnsafeInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/UnnecessaryUnsafeInspection.kt new file mode 100644 index 000000000..ce7ff7a87 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/UnnecessaryUnsafeInspection.kt @@ -0,0 +1,109 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.fabric.util.isFabric +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.isConstructor +import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.findInspection +import com.demonwav.mcdev.util.ifEmpty +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor +import java.awt.FlowLayout +import javax.swing.JCheckBox +import javax.swing.JComponent +import javax.swing.JPanel + +class UnnecessaryUnsafeInspection : MixinInspection() { + @JvmField + var alwaysUnnecessaryOnFabric = true + + override fun getStaticDescription() = "Reports unnecessary unsafe = true" + + override fun createOptionsPanel(): JComponent { + val panel = JPanel(FlowLayout(FlowLayout.LEFT)) + val checkbox = JCheckBox("Always unnecessary in Fabric mods", alwaysUnnecessaryOnFabric) + checkbox.addActionListener { + alwaysUnnecessaryOnFabric = checkbox.isSelected + } + panel.add(checkbox) + return panel + } + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { + val isFabric = holder.file.isFabric + val alwaysUnnecessary = isFabric && alwaysUnnecessaryOnFabric + val requiresUnsafeForCtorHeadOnFabric = + holder.project.findInspection(CtorHeadNoUnsafeInspection.SHORT_NAME) + ?.ignoreForFabric == false + + return object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + if ((!alwaysUnnecessary || requiresUnsafeForCtorHeadOnFabric) && + annotation.findDeclaredAttributeValue("value")?.constantValue == "CTOR_HEAD" + ) { + // this case is handled by a specific inspection for CTOR_HEAD + return + } + + val unsafeValue = annotation.findDeclaredAttributeValue("unsafe") ?: return + if (unsafeValue.constantValue != true) { + return + } + + if (alwaysUnnecessary || !mightTargetConstructor(holder.project, annotation)) { + holder.registerProblem( + unsafeValue, + "Unnecessary unsafe = true", + AnnotationAttributeFix(annotation, "unsafe" to null) + ) + } + } + } + } + + companion object { + fun mightTargetConstructor(project: Project, at: PsiAnnotation): Boolean { + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return true + val handler = injectorAnnotation.qualifiedName?.let { + MixinAnnotationHandler.forMixinAnnotation(it, project) + } ?: return true + + val targets = handler.resolveTarget(injectorAnnotation) + .filterIsInstance() + .ifEmpty { return true } + + return targets.any { it.classAndMethod.method.isConstructor } + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt new file mode 100644 index 000000000..95f724a84 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/LocalArgsOnlyInspection.kt @@ -0,0 +1,75 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.inspection.injector.ModifyVariableArgsOnlyInspection +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.unwrapLocalRef +import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.mapFirstNotNull +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiParameter +import com.intellij.psi.util.parentOfType + +class LocalArgsOnlyInspection : MixinInspection() { + override fun getStaticDescription() = + "Checks that @Local has argsOnly if it targets arguments, which improves performance of the mixin" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(localAnnotation: PsiAnnotation) { + if (localAnnotation.qualifiedName != MixinConstants.MixinExtras.LOCAL) { + return + } + if (localAnnotation.findDeclaredAttributeValue("argsOnly")?.constantValue == true) { + return + } + val parameter = localAnnotation.parentOfType() ?: return + val method = parameter.findContainingMethod() ?: return + + val targets = method.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, holder.project) + ?: return@mapFirstNotNull null + handler.resolveTarget(annotation).asSequence() + .filterIsInstance() + .map { it.classAndMethod } + } ?: return + + val localType = parameter.type.unwrapLocalRef() + + if (ModifyVariableArgsOnlyInspection.shouldReport(localAnnotation, localType, targets)) { + holder.registerProblem( + localAnnotation.nameReferenceElement ?: localAnnotation, + "@Local may be argsOnly = true", + AnnotationAttributeFix(localAnnotation, "argsOnly" to true) + ) + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt index 91c13632f..0d637da9a 100644 --- a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnnecessaryMutableLocalInspection.kt @@ -25,13 +25,13 @@ import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.mixinextras.WrapOperationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.unwrapLocalRef import com.demonwav.mcdev.util.findContainingMethod import com.intellij.codeInspection.LocalQuickFixOnPsiElement import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiClass -import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiExpressionList @@ -40,8 +40,6 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiParameter import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.PsiType -import com.intellij.psi.PsiTypes import com.intellij.psi.search.searches.OverridingMethodsSearch import com.intellij.psi.search.searches.ReferencesSearch import com.intellij.psi.util.PsiUtil @@ -169,8 +167,7 @@ class UnnecessaryMutableLocalInspection : MixinInspection() { private fun fixMethod(method: PsiMethod, paramIndex: Int) { val param = method.parameterList.getParameter(paramIndex) ?: return - val paramType = param.type as? PsiClassType ?: return - val innerType = paramType.innerRefType ?: return + val innerType = param.type.unwrapLocalRef() val factory = PsiElementFactory.getInstance(method.project) param.typeElement?.replace(factory.createTypeElement(innerType)) for (ref in ReferencesSearch.search(param)) { @@ -180,19 +177,5 @@ class UnnecessaryMutableLocalInspection : MixinInspection() { call.replace(ref.element) } } - - private val PsiClassType.innerRefType: PsiType? - get() = - when (resolve()?.qualifiedName?.substringAfterLast('.')) { - "LocalBooleanRef" -> PsiTypes.booleanType() - "LocalCharRef" -> PsiTypes.charType() - "LocalDoubleRef" -> PsiTypes.doubleType() - "LocalFloatRef" -> PsiTypes.floatType() - "LocalIntRef" -> PsiTypes.intType() - "LocalLongRef" -> PsiTypes.longType() - "LocalShortRef" -> PsiTypes.shortType() - "LocalRef" -> parameters.getOrNull(0) - else -> null - } } } diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt new file mode 100644 index 000000000..0904e6110 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/UnresolvedLocalCaptureInspection.kt @@ -0,0 +1,76 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.unwrapLocalRef +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.mapFirstNotNull +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiParameter +import com.intellij.psi.util.parentOfType + +class UnresolvedLocalCaptureInspection : MixinInspection() { + override fun getStaticDescription() = "Verifies targets of @Local annotations" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(localAnnotation: PsiAnnotation) { + if (localAnnotation.qualifiedName != MixinConstants.MixinExtras.LOCAL) { + return + } + + val parameter = localAnnotation.parentOfType() ?: return + val method = parameter.findContainingMethod() ?: return + val targets = method.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = + MixinAnnotationHandler.forMixinAnnotation(qName, holder.project) as? InjectorAnnotationHandler + ?: return@mapFirstNotNull null + handler.resolveInstructions(annotation) + } ?: return + val module = method.findModule() ?: return + + val localInfo = LocalInfo.fromAnnotation(parameter.type.unwrapLocalRef(), localAnnotation) + + for (target in targets) { + val locals = localInfo.getLocals(module, target.method.clazz, target.method.method, target.result.insn) + ?: continue + val matchingLocals = localInfo.matchLocals(locals, CollectVisitor.Mode.MATCH_ALL) + if (matchingLocals.size != 1) { + holder.registerProblem( + localAnnotation.nameReferenceElement ?: localAnnotation, + "@Local does not match any or matched multiple local variables in the target method" + ) + return + } + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt index 348f3f241..4e963eb22 100644 --- a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt +++ b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.reference +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT_CODE import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE @@ -88,20 +89,28 @@ object InjectionPointReference : ReferenceResolver(), MixinReference { getAllAtCodes(context.project).keys.asSequence() .map { PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(it).completeToLiteral(context), + LookupElementBuilder.create(it).completeInjectionPoint(context), 1.0, ) } + getCustomInjectionPointInheritors(context.project).asSequence() .map { PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(it).completeToLiteral(context), + LookupElementBuilder.create(it).completeInjectionPoint(context), 0.0, ) } ).toTypedArray() } + private fun LookupElementBuilder.completeInjectionPoint(context: PsiElement): LookupElementBuilder { + val injectionPoint = InjectionPoint.byAtCode(lookupString) ?: return completeToLiteral(context) + + return completeToLiteral(context) { editor, element -> + injectionPoint.onCompleted(editor, element) + } + } + private val SLICE_SELECTORS_KEY = Key>>("mcdev.sliceSelectors") private fun getSliceSelectors(project: Project): List { diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt new file mode 100644 index 000000000..710e7834e --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt @@ -0,0 +1,151 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.util + +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.util.computeStringArray +import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.descriptor +import com.intellij.openapi.module.Module +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiType +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode + +class LocalInfo( + val type: PsiType?, + val argsOnly: Boolean, + val index: Int?, + val ordinal: Int?, + val names: Set, +) { + fun getLocals( + module: Module, + targetClass: ClassNode, + methodNode: MethodNode, + insn: AbstractInsnNode, + ): Array? { + return if (argsOnly) { + val args = mutableListOf() + if (!methodNode.hasAccess(Opcodes.ACC_STATIC)) { + val thisDesc = Type.getObjectType(targetClass.name).descriptor + args.add(LocalVariables.LocalVariable("this", thisDesc, null, null, null, 0)) + } + for (argType in Type.getArgumentTypes(methodNode.desc)) { + args.add( + LocalVariables.LocalVariable("arg${args.size}", argType.descriptor, null, null, null, args.size), + ) + if (argType.size == 2) { + args.add(null) + } + } + args.toTypedArray() + } else { + LocalVariables.getLocals(module, targetClass, methodNode, insn) + } + } + + fun matchLocals( + locals: Array, + mode: CollectVisitor.Mode, + matchType: Boolean = true, + ): List { + val typeDesc = type?.descriptor + if (ordinal != null) { + val ordinals = mutableMapOf() + val result = mutableListOf() + for (local in locals) { + if (local == null) { + continue + } + val ordinal = ordinals[local.desc] ?: 0 + ordinals[local.desc!!] = ordinal + 1 + if (ordinal == this.ordinal && (!matchType || typeDesc == null || local.desc == typeDesc)) { + result += local + } + } + return result + } + + if (index != null) { + val local = locals.firstOrNull { it?.index == index } + if (local != null) { + if (!matchType || typeDesc == null || local.desc == typeDesc) { + return listOf(local) + } + } + return emptyList() + } + + if (names.isNotEmpty()) { + val result = mutableListOf() + for (local in locals) { + if (local == null) { + continue + } + if (names.contains(local.name)) { + if (!matchType || typeDesc == null || local.desc == typeDesc) { + result += local + } + } + } + return result + } + + // implicit mode + if (mode == CollectVisitor.Mode.COMPLETION) { + return locals.asSequence() + .filterNotNull() + .filter { local -> locals.count { it?.desc == local.desc } == 1 } + .toList() + } + + return if (matchType && typeDesc != null) { + locals.singleOrNull { it?.desc == typeDesc }?.let { listOf(it) } ?: emptyList() + } else { + emptyList() + } + } + + companion object { + /** + * Gets a [LocalInfo] from an annotation which declares the following attributes: + * - `argsOnly` to only match the target method arguments + * - `index` to match local variables by index + * - `ordinal` to match local variables by type then index + * - `name` to match local variables by name + * + * The `ModifyVariable` and `Local` annotations match this description. + */ + fun fromAnnotation(localType: PsiType?, annotation: PsiAnnotation): LocalInfo { + val argsOnly = annotation.findDeclaredAttributeValue("argsOnly")?.constantValue as? Boolean ?: false + val index = (annotation.findDeclaredAttributeValue("index")?.constantValue as? Int) + ?.takeIf { it != -1 } + val ordinal = (annotation.findDeclaredAttributeValue("ordinal")?.constantValue as? Int) + ?.takeIf { it != -1 } + val names = annotation.findDeclaredAttributeValue("name")?.computeStringArray()?.toSet() ?: emptySet() + return LocalInfo(localType, argsOnly, index, ordinal, names) + } + } +} diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt index 3299e11fc..937cf44bc 100644 --- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt +++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt @@ -20,6 +20,10 @@ package com.demonwav.mcdev.platform.mixin.util +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes + @Suppress("MemberVisibilityCanBePrivate") object MixinConstants { const val PACKAGE = "org.spongepowered.asm.mixin." @@ -81,5 +85,26 @@ object MixinConstants { const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation" const val LOCAL = "com.llamalad7.mixinextras.sugar.Local" const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref." + + fun PsiType.unwrapLocalRef(): PsiType { + if (this !is PsiClassType) { + return this + } + val qName = resolve()?.qualifiedName ?: return this + if (!qName.startsWith(LOCAL_REF_PACKAGE)) { + return this + } + return when (qName.substringAfterLast('.')) { + "LocalBooleanRef" -> PsiTypes.booleanType() + "LocalCharRef" -> PsiTypes.charType() + "LocalDoubleRef" -> PsiTypes.doubleType() + "LocalFloatRef" -> PsiTypes.floatType() + "LocalIntRef" -> PsiTypes.intType() + "LocalLongRef" -> PsiTypes.longType() + "LocalShortRef" -> PsiTypes.shortType() + "LocalRef" -> parameters.getOrNull(0) ?: this + else -> this + } + } } } diff --git a/src/main/kotlin/platform/neoforge/NeoForgeFileIconProvider.kt b/src/main/kotlin/platform/neoforge/NeoForgeFileIconProvider.kt new file mode 100644 index 000000000..c292104c6 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/NeoForgeFileIconProvider.kt @@ -0,0 +1,49 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.facet.MinecraftFacet +import com.intellij.ide.FileIconProvider +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.openapi.vfs.VirtualFile +import javax.swing.Icon + +class NeoForgeFileIconProvider : FileIconProvider { + + override fun getIcon(file: VirtualFile, @Iconable.IconFlags flags: Int, project: Project?): Icon? { + project ?: return null + + if (!MinecraftSettings.instance.isShowProjectPlatformIcons) { + return null + } + + val module = ModuleUtilCore.findModuleForFile(file, project) ?: return null + val forgeModule = MinecraftFacet.getInstance(module, NeoForgeModuleType) ?: return null + + if (file == forgeModule.mcmod) { + return forgeModule.icon + } + return null + } +} diff --git a/src/main/kotlin/platform/neoforge/NeoForgeModule.kt b/src/main/kotlin/platform/neoforge/NeoForgeModule.kt new file mode 100644 index 000000000..56341c4a1 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/NeoForgeModule.kt @@ -0,0 +1,131 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.inspection.IsCancelled +import com.demonwav.mcdev.platform.AbstractModule +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants +import com.demonwav.mcdev.util.SourceType +import com.demonwav.mcdev.util.createVoidMethodWithParameterType +import com.demonwav.mcdev.util.nullable +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions +import com.demonwav.mcdev.util.runWriteTaskLater +import com.demonwav.mcdev.util.waitForAllSmart +import com.intellij.json.JsonFileType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UIdentifier +import org.jetbrains.uast.toUElementOfType + +class NeoForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(facet) { + + var mcmod by nullable { facet.findFile(NeoForgeConstants.MCMOD_INFO, SourceType.RESOURCE) } + private set + + override val moduleType = NeoForgeModuleType + override val type = PlatformType.NEOFORGE + override val icon = PlatformAssets.NEOFORGE_ICON + + override fun init() { + ApplicationManager.getApplication().executeOnPooledThread { + waitForAllSmart() + // Set mcmod.info icon + runWriteTaskLater { + FileTypeManager.getInstance().associatePattern(JsonFileType.INSTANCE, NeoForgeConstants.MCMOD_INFO) + FileTypeManager.getInstance().associatePattern(JsonFileType.INSTANCE, NeoForgeConstants.PACK_MCMETA) + } + } + } + + override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?): Boolean { + if (method == null) { + return NeoForgeConstants.FML_EVENT == eventClass.qualifiedName || + NeoForgeConstants.EVENTBUS_EVENT == eventClass.qualifiedName + } + + if (method.hasAnnotation(NeoForgeConstants.SUBSCRIBE_EVENT)) { + return NeoForgeConstants.EVENTBUS_EVENT == eventClass.qualifiedName + } + + // just default to true + return true + } + + override fun writeErrorMessageForEventParameter(eventClass: PsiClass, method: PsiMethod): String { + return formatWrongEventMessage( + NeoForgeConstants.EVENTBUS_EVENT, + NeoForgeConstants.SUBSCRIBE_EVENT, + NeoForgeConstants.EVENTBUS_EVENT == eventClass.qualifiedName, + ) + } + + private fun formatWrongEventMessage(expected: String, suggested: String, wrong: Boolean): String { + val base = "Parameter is not a subclass of $expected\n" + if (wrong) { + return base + "This method should be annotated with $suggested" + } + return base + "Compiling and running this listener may result in a runtime exception" + } + + override fun isStaticListenerSupported(method: PsiMethod) = true + + override fun generateEventListenerMethod( + containingClass: PsiClass, + chosenClass: PsiClass, + chosenName: String, + data: GenerationData?, + ): PsiMethod? { + val method = createVoidMethodWithParameterType(project, chosenName, chosenClass) ?: return null + val modifierList = method.modifierList + + modifierList.addAnnotation(NeoForgeConstants.SUBSCRIBE_EVENT) + + return method + } + + override fun shouldShowPluginIcon(element: PsiElement?): Boolean { + val identifier = element?.toUElementOfType() + ?: return false + + val psiClass = runCatchingKtIdeaExceptions { identifier.uastParent as? UClass } + ?: return false + + return !psiClass.hasModifier(JvmModifier.ABSTRACT) && + psiClass.findAnnotation(NeoForgeConstants.MOD_ANNOTATION) != null + } + + override fun checkUselessCancelCheck(expression: PsiMethodCallExpression): IsCancelled? = null + + override fun dispose() { + mcmod = null + super.dispose() + } +} diff --git a/src/main/kotlin/platform/neoforge/NeoForgeModuleType.kt b/src/main/kotlin/platform/neoforge/NeoForgeModuleType.kt new file mode 100644 index 000000000..f02a84422 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/NeoForgeModuleType.kt @@ -0,0 +1,52 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.AbstractModuleType +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants +import com.intellij.psi.PsiClass + +object NeoForgeModuleType : AbstractModuleType("", "") { + + private const val ID = "NEOFORGE_MODULE_TYPE" + + private val IGNORED_ANNOTATIONS = listOf( + NeoForgeConstants.MOD_ANNOTATION, + NeoForgeConstants.SUBSCRIBE_EVENT, + NeoForgeConstants.EVENT_BUS_SUBSCRIBER, + ) + private val LISTENER_ANNOTATIONS = listOf( + NeoForgeConstants.SUBSCRIBE_EVENT, + ) + + override val platformType = PlatformType.NEOFORGE + override val icon = PlatformAssets.NEOFORGE_ICON + override val id = ID + override val ignoredAnnotations = IGNORED_ANNOTATIONS + override val listenerAnnotations = LISTENER_ANNOTATIONS + override val isEventGenAvailable = true + + override fun generateModule(facet: MinecraftFacet) = NeoForgeModule(facet) + override fun getDefaultListenerName(psiClass: PsiClass): String = defaultNameForSubClassEvents(psiClass) +} diff --git a/src/main/kotlin/platform/neoforge/creator/asset-steps.kt b/src/main/kotlin/platform/neoforge/creator/asset-steps.kt new file mode 100644 index 000000000..64cd30844 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/creator/asset-steps.kt @@ -0,0 +1,158 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.creator + +import com.demonwav.mcdev.creator.addLicense +import com.demonwav.mcdev.creator.addTemplates +import com.demonwav.mcdev.creator.buildsystem.AbstractBuildSystemStep +import com.demonwav.mcdev.creator.buildsystem.AbstractRunBuildSystemStep +import com.demonwav.mcdev.creator.buildsystem.BuildSystemPropertiesStep +import com.demonwav.mcdev.creator.buildsystem.BuildSystemSupport +import com.demonwav.mcdev.creator.findStep +import com.demonwav.mcdev.creator.splitPackage +import com.demonwav.mcdev.creator.step.AbstractLongRunningAssetsStep +import com.demonwav.mcdev.creator.step.AbstractModIdStep +import com.demonwav.mcdev.creator.step.AbstractModNameStep +import com.demonwav.mcdev.creator.step.AbstractReformatFilesStep +import com.demonwav.mcdev.creator.step.AuthorsStep +import com.demonwav.mcdev.creator.step.DescriptionStep +import com.demonwav.mcdev.creator.step.LicenseStep +import com.demonwav.mcdev.creator.step.MainClassStep +import com.demonwav.mcdev.creator.step.UpdateUrlStep +import com.demonwav.mcdev.creator.step.UseMixinsStep +import com.demonwav.mcdev.creator.step.WebsiteStep +import com.demonwav.mcdev.platform.forge.util.ForgePackDescriptor +import com.demonwav.mcdev.util.MinecraftTemplates +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.project.Project + +class NeoForgeProjectFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningAssetsStep(parent) { + override val description = "Creating NeoForge project files" + + override fun setupAssets(project: Project) { + val mcVersion = data.getUserData(NeoForgeVersionChainStep.MC_VERSION_KEY) ?: return + val forgeVersion = data.getUserData(NeoForgeVersionChainStep.NEOFORGE_VERSION_KEY) ?: return + val mainClass = data.getUserData(MainClassStep.KEY) ?: return + val (mainPackageName, mainClassName) = splitPackage(mainClass) + val buildSystemProps = findStep>() + val modId = data.getUserData(AbstractModIdStep.KEY) ?: return + val modName = data.getUserData(AbstractModNameStep.KEY) ?: return + val license = data.getUserData(LicenseStep.KEY) ?: return + val description = data.getUserData(DescriptionStep.KEY) ?: "" + val updateUrl = data.getUserData(UpdateUrlStep.KEY) ?: "" + val authors = data.getUserData(AuthorsStep.KEY) ?: emptyList() + val website = data.getUserData(WebsiteStep.KEY) ?: "" + val useMixins = data.getUserData(UseMixinsStep.KEY) ?: false + + val nextMcVersion = when (val part = mcVersion.parts.getOrNull(1)) { + // Extract the major version and increment (1.20.4 -> 1.21), as is done manually in the MDK + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + val packDescriptor = ForgePackDescriptor.forMcVersion(mcVersion) ?: ForgePackDescriptor.FORMAT_3 + + assets.addTemplateProperties( + "PACKAGE_NAME" to mainPackageName, + "CLASS_NAME" to mainClassName, + "ARTIFACT_ID" to buildSystemProps.artifactId, + "MOD_ID" to modId, + "MOD_NAME" to modName, + "MOD_VERSION" to buildSystemProps.version, + "NEOFORGE_SPEC_VERSION" to forgeVersion.parts[0].versionString, + "MC_VERSION" to mcVersion, + "MC_NEXT_VERSION" to "1.$nextMcVersion", + "LICENSE" to license, + "DESCRIPTION" to description, + "MIXIN_CONFIG" to if (useMixins) "$modId.mixins.json" else null, + "PACK_FORMAT" to packDescriptor.format, + "PACK_COMMENT" to packDescriptor.comment, + ) + + if (updateUrl.isNotBlank()) { + assets.addTemplateProperties("UPDATE_URL" to updateUrl) + } + + if (authors.isNotEmpty()) { + assets.addTemplateProperties("AUTHOR_LIST" to authors.joinToString(", ")) + } + + if (website.isNotBlank()) { + assets.addTemplateProperties("WEBSITE" to website) + } + + val mainClassTemplate = MinecraftTemplates.NEOFORGE_MAIN_CLASS_TEMPLATE + + assets.addTemplates( + project, + "src/main/java/${mainClass.replace('.', '/')}.java" to mainClassTemplate, + "src/main/resources/pack.mcmeta" to MinecraftTemplates.NEOFORGE_PACK_MCMETA_TEMPLATE, + "src/main/resources/META-INF/mods.toml" to MinecraftTemplates.NEOFORGE_MODS_TOML_TEMPLATE, + ) + + val configPath = if (mainPackageName != null) { + "src/main/java/${mainPackageName.replace('.', '/')}/Config.java" + } else { + "src/main/java/Config.java" + } + assets.addTemplates(project, configPath to MinecraftTemplates.NEOFORGE_CONFIG_TEMPLATE) + + assets.addLicense(project) + } +} + +// Needs to be a separate step from above because of PACKAGE_NAME being different +class NeoForgeMixinsJsonStep(parent: NewProjectWizardStep) : AbstractLongRunningAssetsStep(parent) { + override val description = "Creating mixins json" + + override fun setupAssets(project: Project) { + val useMixins = data.getUserData(UseMixinsStep.KEY) ?: false + if (useMixins) { + val modId = data.getUserData(AbstractModIdStep.KEY) ?: return + val buildSystemProps = findStep>() + assets.addTemplateProperties( + "PACKAGE_NAME" to "${buildSystemProps.groupId}.$modId.mixin", + "MOD_ID" to buildSystemProps.artifactId, + ) + val mixinsJsonFile = "src/main/resources/$modId.mixins.json" + assets.addTemplates(project, mixinsJsonFile to MinecraftTemplates.NEOFORGE_MIXINS_JSON_TEMPLATE) + } + } +} + +class NeoForgeReformatPackDescriptorStep(parent: NewProjectWizardStep) : AbstractReformatFilesStep(parent) { + + override fun addFilesToReformat() { + addFileToReformat("src/main/resources/pack.mcmeta") + } +} + +class NeoForgeBuildSystemStep(parent: NewProjectWizardStep) : AbstractBuildSystemStep(parent) { + override val platformName = "NeoForge" +} + +class NeoForgePostBuildSystemStep( + parent: NewProjectWizardStep, +) : AbstractRunBuildSystemStep(parent, NeoForgeBuildSystemStep::class.java) { + override val step = BuildSystemSupport.POST_STEP +} diff --git a/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt b/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt new file mode 100644 index 000000000..f64e676be --- /dev/null +++ b/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt @@ -0,0 +1,153 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.creator + +import com.demonwav.mcdev.creator.EmptyStep +import com.demonwav.mcdev.creator.ParchmentStep +import com.demonwav.mcdev.creator.addGradleGitignore +import com.demonwav.mcdev.creator.addTemplates +import com.demonwav.mcdev.creator.buildsystem.AbstractRunGradleTaskStep +import com.demonwav.mcdev.creator.buildsystem.BuildSystemPropertiesStep +import com.demonwav.mcdev.creator.buildsystem.BuildSystemSupport +import com.demonwav.mcdev.creator.buildsystem.GRADLE_VERSION_KEY +import com.demonwav.mcdev.creator.buildsystem.GradleImportStep +import com.demonwav.mcdev.creator.buildsystem.GradleWrapperStep +import com.demonwav.mcdev.creator.buildsystem.addGradleWrapperProperties +import com.demonwav.mcdev.creator.findStep +import com.demonwav.mcdev.creator.gitEnabled +import com.demonwav.mcdev.creator.step.AbstractLongRunningAssetsStep +import com.demonwav.mcdev.creator.step.AbstractModIdStep +import com.demonwav.mcdev.creator.step.AbstractModNameStep +import com.demonwav.mcdev.creator.step.AuthorsStep +import com.demonwav.mcdev.creator.step.DescriptionStep +import com.demonwav.mcdev.creator.step.LicenseStep +import com.demonwav.mcdev.creator.step.NewProjectWizardChainStep.Companion.nextStep +import com.demonwav.mcdev.util.MinecraftTemplates +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.util.lang.JavaVersion + +private val ngWrapperVersion = SemanticVersion.release(8, 4) + +const val MAGIC_RUN_CONFIGS_FILE = ".hello_from_mcdev" + +class NeoForgeGradleSupport : BuildSystemSupport { + override val preferred = true + + override fun createStep(step: String, parent: NewProjectWizardStep): NewProjectWizardStep { + return when (step) { + BuildSystemSupport.PRE_STEP -> NeoForgeGradleFilesStep(parent).nextStep(::GradleWrapperStep) + BuildSystemSupport.POST_STEP -> NeoForgeCompileJavaStep(parent).nextStep(::GradleImportStep) + else -> EmptyStep(parent) + } + } +} + +class NeoForgeGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningAssetsStep(parent) { + override val description = "Creating Gradle files" + + override fun setupAssets(project: Project) { + val mcVersion = data.getUserData(NeoForgeVersionChainStep.MC_VERSION_KEY) ?: return + val neoforgeVersion = data.getUserData(NeoForgeVersionChainStep.NEOFORGE_VERSION_KEY) ?: return + val modId = data.getUserData(AbstractModIdStep.KEY) ?: return + val modName = data.getUserData(AbstractModNameStep.KEY) ?: return + val buildSystemProps = findStep>() + val javaVersion = context.projectJdk?.versionString?.let(JavaVersion::parse) + val authors = data.getUserData(AuthorsStep.KEY) ?: emptyList() + val description = data.getUserData(DescriptionStep.KEY) ?: return + val license = data.getUserData(LicenseStep.KEY) ?: return + val parchment = findStep() + val mcNextVersionPart = mcVersion.parts[1] + val mcNextVersion = if (mcNextVersionPart is SemanticVersion.Companion.VersionPart.ReleasePart) { + SemanticVersion.release(1, mcNextVersionPart.version + 1) + } else { + mcVersion + } + + data.putUserData(GRADLE_VERSION_KEY, ngWrapperVersion) + + assets.addTemplateProperties( + "MOD_ID" to modId, + "MOD_NAME" to modName, + "MC_VERSION" to mcVersion, + "MC_NEXT_VERSION" to mcNextVersion, + "NEOFORGE_VERSION" to neoforgeVersion, + "NEOFORGE_SPEC_VERSION" to neoforgeVersion.parts[0].versionString, + "GROUP_ID" to buildSystemProps.groupId, + "ARTIFACT_ID" to buildSystemProps.artifactId, + "MOD_VERSION" to buildSystemProps.version, + "DESCRIPTION" to description, + "AUTHOR_LIST" to authors.joinToString(", "), + "LICENSE" to license.id, + "HAS_DATA" to "true" + ) + + if (parchment.useParchment) { + assets.addTemplateProperties( + "PARCHMENT_MC_VERSION" to parchment.parchmentVersion?.mcVersion.toString(), + "PARCHMENT_VERSION" to parchment.parchmentVersion?.parchmentVersion, + ) + } + + if (javaVersion != null) { + assets.addTemplateProperties("JAVA_VERSION" to javaVersion.feature) + } + + if (neoforgeVersion >= SemanticVersion.release(39, 0, 88)) { + assets.addTemplateProperties("GAME_TEST_FRAMEWORK" to "true") + } + + assets.addTemplates( + project, + "build.gradle" to MinecraftTemplates.NEOFORGE_BUILD_GRADLE_TEMPLATE, + "gradle.properties" to MinecraftTemplates.NEOFORGE_GRADLE_PROPERTIES_TEMPLATE, + "settings.gradle" to MinecraftTemplates.NEOFORGE_SETTINGS_GRADLE_TEMPLATE, + ) + + assets.addGradleWrapperProperties(project) + + if (gitEnabled) { + assets.addGradleGitignore(project) + } + + WriteAction.runAndWait { + val dir = VfsUtil.createDirectoryIfMissing( + LocalFileSystem.getInstance(), + "${assets.outputDirectory}/.gradle", + ) + ?: throw IllegalStateException("Unable to create .gradle directory") + val file = dir.findOrCreateChildData(this, MAGIC_RUN_CONFIGS_FILE) + val fileContents = buildSystemProps.artifactId + "\n" + + mcVersion + "\n" + + neoforgeVersion + "\n" + + "genIntellijRuns" + VfsUtil.saveText(file, fileContents) + } + } +} + +class NeoForgeCompileJavaStep(parent: NewProjectWizardStep) : AbstractRunGradleTaskStep(parent) { + override val task = "compileJava" +} diff --git a/src/main/kotlin/platform/neoforge/creator/ui-steps.kt b/src/main/kotlin/platform/neoforge/creator/ui-steps.kt new file mode 100644 index 000000000..86755fed2 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/creator/ui-steps.kt @@ -0,0 +1,112 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.creator + +import com.demonwav.mcdev.creator.ParchmentStep +import com.demonwav.mcdev.creator.platformtype.ModPlatformStep +import com.demonwav.mcdev.creator.step.AbstractCollapsibleStep +import com.demonwav.mcdev.creator.step.AbstractLatentStep +import com.demonwav.mcdev.creator.step.AbstractMcVersionChainStep +import com.demonwav.mcdev.creator.step.AuthorsStep +import com.demonwav.mcdev.creator.step.DescriptionStep +import com.demonwav.mcdev.creator.step.ForgeStyleModIdStep +import com.demonwav.mcdev.creator.step.LicenseStep +import com.demonwav.mcdev.creator.step.MainClassStep +import com.demonwav.mcdev.creator.step.ModNameStep +import com.demonwav.mcdev.creator.step.NewProjectWizardChainStep.Companion.nextStep +import com.demonwav.mcdev.creator.step.UpdateUrlStep +import com.demonwav.mcdev.creator.step.UseMixinsStep +import com.demonwav.mcdev.creator.step.WebsiteStep +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.demonwav.mcdev.util.asyncIO +import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.util.IncorrectOperationException +import kotlinx.coroutines.coroutineScope + +private val minSupportedMcVersion = MinecraftVersions.MC1_20_2 + +class NeoForgePlatformStep(parent: ModPlatformStep) : AbstractLatentStep(parent) { + override val description = "fetch NeoForge versions" + + override suspend fun computeData() = coroutineScope { + asyncIO { NeoForgeVersion.downloadData() }.await() + } + + override fun createStep(data: NeoForgeVersion) = NeoForgeVersionChainStep(this, data) + .nextStep(::ForgeStyleModIdStep) + .nextStep(::ModNameStep) + .nextStep(::MainClassStep) + .nextStep(::UseMixinsStep) + .nextStep(::LicenseStep) + .nextStep(::NeoForgeOptionalSettingsStep) + .nextStep(::NeoForgeBuildSystemStep) + .nextStep(::NeoForgeProjectFilesStep) + .nextStep(::NeoForgeMixinsJsonStep) + .nextStep(::NeoForgePostBuildSystemStep) + .nextStep(::NeoForgeReformatPackDescriptorStep) + + class Factory : ModPlatformStep.Factory { + override val name = "NeoForge" + override fun createStep(parent: ModPlatformStep) = NeoForgePlatformStep(parent) + } +} + +class NeoForgeVersionChainStep( + parent: NewProjectWizardStep, + private val neoforgeVersionData: NeoForgeVersion, +) : AbstractMcVersionChainStep(parent, "NeoForge Version:") { + companion object { + private const val NEOFORGE_VERSION = 1 + + val MC_VERSION_KEY = Key.create("${NeoForgeVersionChainStep::class.java}.mcVersion") + val NEOFORGE_VERSION_KEY = + Key.create("${NeoForgeVersionChainStep::class.java}.neoforgeVersion") + } + + override fun getAvailableVersions(versionsAbove: List>): List> { + return when (versionsAbove.size) { + MINECRAFT_VERSION -> neoforgeVersionData.sortedMcVersions.filter { it >= minSupportedMcVersion } + NEOFORGE_VERSION -> + neoforgeVersionData.getNeoForgeVersions(versionsAbove[MINECRAFT_VERSION] as SemanticVersion) + else -> throw IncorrectOperationException() + } + } + + override fun setupProject(project: Project) { + super.setupProject(project) + data.putUserData(MC_VERSION_KEY, getVersion(MINECRAFT_VERSION) as SemanticVersion) + data.putUserData(NEOFORGE_VERSION_KEY, getVersion(NEOFORGE_VERSION) as SemanticVersion) + } +} + +class NeoForgeOptionalSettingsStep(parent: NewProjectWizardStep) : AbstractCollapsibleStep(parent) { + override val title = "Optional Settings" + + override fun createStep() = DescriptionStep(this) + .nextStep(::AuthorsStep) + .nextStep(::WebsiteStep) + .nextStep(::UpdateUrlStep) + .nextStep(::ParchmentStep) +} diff --git a/src/main/kotlin/platform/neoforge/framework/NeoForgeLibraryKind.kt b/src/main/kotlin/platform/neoforge/framework/NeoForgeLibraryKind.kt new file mode 100644 index 000000000..c57bc00cf --- /dev/null +++ b/src/main/kotlin/platform/neoforge/framework/NeoForgeLibraryKind.kt @@ -0,0 +1,26 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.framework + +import com.demonwav.mcdev.util.libraryKind +import com.intellij.openapi.roots.libraries.LibraryKind + +val NEOFORGE_LIBRARY_KIND: LibraryKind = libraryKind("neoforge-library") diff --git a/src/main/kotlin/platform/neoforge/framework/NeoForgePresentationProvider.kt b/src/main/kotlin/platform/neoforge/framework/NeoForgePresentationProvider.kt new file mode 100644 index 000000000..c0ffa665a --- /dev/null +++ b/src/main/kotlin/platform/neoforge/framework/NeoForgePresentationProvider.kt @@ -0,0 +1,43 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.framework + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants +import com.demonwav.mcdev.util.localFile +import com.intellij.framework.library.LibraryVersionProperties +import com.intellij.openapi.roots.libraries.LibraryPresentationProvider +import com.intellij.openapi.util.io.JarUtil +import com.intellij.openapi.vfs.VirtualFile + +class NeoForgePresentationProvider : LibraryPresentationProvider(NEOFORGE_LIBRARY_KIND) { + + override fun getIcon(properties: LibraryVersionProperties?) = PlatformAssets.NEOFORGE_ICON + + override fun detect(classesRoots: List): LibraryVersionProperties? { + for (classesRoot in classesRoots) { + if (JarUtil.containsClass(classesRoot.localFile, NeoForgeConstants.MOD_ANNOTATION)) { + return LibraryVersionProperties() + } + } + return null + } +} diff --git a/src/main/kotlin/platform/neoforge/gradle/NeoForgeRunConfigDataService.kt b/src/main/kotlin/platform/neoforge/gradle/NeoForgeRunConfigDataService.kt new file mode 100644 index 000000000..5b42d0751 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/gradle/NeoForgeRunConfigDataService.kt @@ -0,0 +1,158 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.gradle + +import com.demonwav.mcdev.platform.neoforge.creator.MAGIC_RUN_CONFIGS_FILE +import com.demonwav.mcdev.util.invokeAndWait +import com.demonwav.mcdev.util.localFile +import com.demonwav.mcdev.util.runGradleTaskAndWait +import com.intellij.execution.RunManager +import com.intellij.execution.RunManagerListener +import com.intellij.execution.RunnerAndConfigurationSettings +import com.intellij.execution.application.ApplicationConfiguration +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.ProjectKeys +import com.intellij.openapi.externalSystem.model.project.ProjectData +import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider +import com.intellij.openapi.externalSystem.service.project.manage.AbstractProjectDataService +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.LocalFileSystem +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicInteger +import org.jetbrains.plugins.gradle.util.GradleConstants + +class NeoForgeRunConfigDataService : AbstractProjectDataService() { + + override fun getTargetDataKey() = ProjectKeys.PROJECT + + override fun postProcess( + toImport: Collection>, + projectData: ProjectData?, + project: Project, + modelsProvider: IdeModifiableModelsProvider, + ) { + if (projectData == null || projectData.owner != GradleConstants.SYSTEM_ID) { + return + } + + val baseDir = project.guessProjectDir() ?: return + val baseDirPath = baseDir.localFile.toPath() + val hello = baseDirPath.resolve(Paths.get(".gradle", MAGIC_RUN_CONFIGS_FILE)) + if (!Files.isRegularFile(hello)) { + return + } + + val lines = Files.readAllLines(hello, Charsets.UTF_8) + if (lines.size < 4) { + return + } + + val (moduleName, mcVersion, forgeVersion, task) = lines + + val moduleMap = modelsProvider.modules.associateBy { it.name } + val module = moduleMap[moduleName] ?: return + + // We've found the module we were expecting, so we can assume the project imported correctly + Files.delete(hello) + + genIntellijRuns(project, moduleMap, module, task) + } + + private fun genIntellijRuns( + project: Project, + moduleMap: Map, + module: Module, + task: String, + ) { + val mainModule = findMainModule(moduleMap, module) + + ProgressManager.getInstance().run( + object : Task.Backgroundable(project, "genIntellijRuns", false) { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = true + + val projectDir = project.guessProjectDir() ?: return + indicator.text = "Creating run configurations" + indicator.text2 = "Running Gradle task: '$task'" + runGradleTaskAndWait(project, projectDir.localFile.toPath()) { settings -> + settings.taskNames = listOf(task) + } + + cleanupGeneratedRuns(project, mainModule) + } + }, + ) + } + + private fun cleanupGeneratedRuns(project: Project, module: Module) { + invokeAndWait { + if (!module.isDisposed) { + NeoForgeRunManagerListener(module, true) + } + } + + project.guessProjectDir()?.let { dir -> + LocalFileSystem.getInstance().refreshFiles(listOf(dir), true, true, null) + } + } + + private fun findMainModule(moduleMap: Map, module: Module): Module { + return moduleMap[module.name + ".main"] ?: module + } +} + +class NeoForgeRunManagerListener(private val module: Module, hasData: Boolean) : RunManagerListener { + private val count = AtomicInteger(3) + private val disposable = Disposer.newDisposable() + + init { + Disposer.register(module, disposable) + module.project.messageBus.connect(disposable).subscribe(RunManagerListener.TOPIC, this) + // If we don't have a data run, don't wait for it + if (!hasData) { + count.decrementAndGet() + } + } + + override fun runConfigurationAdded(settings: RunnerAndConfigurationSettings) { + val config = settings.configuration as? ApplicationConfiguration ?: return + + val postFixes = arrayOf("runClient", "runServer", "runData") + if (postFixes.none { settings.name.endsWith(it) }) { + return + } + + config.isAllowRunningInParallel = false + config.setModule(module) + RunManager.getInstance(module.project).addConfiguration(settings) + + if (count.decrementAndGet() == 0) { + Disposer.dispose(disposable) + } + } +} diff --git a/src/main/kotlin/platform/neoforge/insight/NeoForgeImplicitUsageProvider.kt b/src/main/kotlin/platform/neoforge/insight/NeoForgeImplicitUsageProvider.kt new file mode 100644 index 000000000..d10ec3bdd --- /dev/null +++ b/src/main/kotlin/platform/neoforge/insight/NeoForgeImplicitUsageProvider.kt @@ -0,0 +1,44 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.insight + +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants +import com.demonwav.mcdev.util.extendsOrImplements +import com.intellij.codeInsight.daemon.ImplicitUsageProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod + +class NeoForgeImplicitUsageProvider : ImplicitUsageProvider { + + override fun isImplicitUsage(element: PsiElement) = isNetworkMessageOrHandler(element) + + private fun isNetworkMessageOrHandler(element: PsiElement): Boolean { + if (element !is PsiMethod || element.isConstructor && element.hasParameters()) { + return false + } + + val containingClass = element.containingClass ?: return false + return containingClass.extendsOrImplements(NeoForgeConstants.NETWORK_MESSAGE) + } + + override fun isImplicitRead(element: PsiElement) = false + override fun isImplicitWrite(element: PsiElement) = false +} diff --git a/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt b/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt new file mode 100644 index 000000000..a84ee784a --- /dev/null +++ b/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt @@ -0,0 +1,33 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.util + +object NeoForgeConstants { + + const val MOD_ANNOTATION = "net.neoforged.fml.common.Mod" + const val SUBSCRIBE_EVENT = "net.neoforged.bus.api.SubscribeEvent" + const val EVENT_BUS_SUBSCRIBER = "net.neoforged.fml.common.Mod.EventBusSubscriber" + const val FML_EVENT = "net.neoforged.fml.event.IModBusEvent" + const val EVENTBUS_EVENT = "net.neoforged.bus.api.Event" + const val NETWORK_MESSAGE = "net.neoforged.neoforge.network.simple.SimpleMessage" + const val MCMOD_INFO = "mcmod.info" + const val PACK_MCMETA = "pack.mcmeta" +} diff --git a/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt b/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt new file mode 100644 index 000000000..e1b6427be --- /dev/null +++ b/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt @@ -0,0 +1,75 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.neoforge.version + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.diagnostic.logger +import java.io.IOException + +class NeoForgeVersion private constructor(val versions: List) { + + val sortedMcVersions: List by lazy { + val version = versions.asSequence() + .map { it.substringBeforeLast('.') } + .distinct() + .mapNotNull { + val shortVersion = SemanticVersion.tryParse(it) + if (shortVersion != null) { + val parts = shortVersion.parts.toMutableList() + // Insert the '1.' part to the base neoforge version + parts.add(0, SemanticVersion.Companion.VersionPart.ReleasePart(1, "1")) + SemanticVersion(parts) + } else null + }.distinct() + .sortedDescending() + .toList() + return@lazy version + } + + fun getNeoForgeVersions(mcVersion: SemanticVersion): List { + val versionText = mcVersion.toString() + // Drop the 1. part of the mc version + val shortMcVersion = versionText.substringAfter('.') + val toList = versions.asSequence() + .filter { it.substringBeforeLast('.') == shortMcVersion } + .mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + .take(50) + .toList() + return toList + } + + companion object { + private val LOGGER = logger() + + suspend fun downloadData(): NeoForgeVersion? { + try { + val url = "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml" + val versions = collectMavenVersions(url) + return NeoForgeVersion(versions) + } catch (e: IOException) { + LOGGER.error("Failed to retrieve NeoForge version data", e) + } + return null + } + } +} diff --git a/src/main/kotlin/util/MinecraftTemplates.kt b/src/main/kotlin/util/MinecraftTemplates.kt index d297cb665..368e30cee 100644 --- a/src/main/kotlin/util/MinecraftTemplates.kt +++ b/src/main/kotlin/util/MinecraftTemplates.kt @@ -111,6 +111,24 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory { mixinGroup.addTemplate(FileTemplateDescriptor(MIXIN_OVERWRITE_FALLBACK)) } + FileTemplateGroupDescriptor("NeoForge", PlatformAssets.NEOFORGE_ICON).let { forgeGroup -> + group.addTemplate(forgeGroup) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_MIXINS_JSON_TEMPLATE, PlatformAssets.NEOFORGE_ICON)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_MAIN_CLASS_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_CONFIG_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_BUILD_GRADLE_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_GRADLE_PROPERTIES_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_SETTINGS_GRADLE_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_MODS_TOML_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_PACK_MCMETA_TEMPLATE)) + + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_BLOCK_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_ITEM_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_PACKET_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_ENCHANTMENT_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(NEOFORGE_MOB_EFFECT_TEMPLATE)) + } + FileTemplateGroupDescriptor("Common", PlatformAssets.MINECRAFT_ICON).let { commonGroup -> group.addTemplate(commonGroup) commonGroup.addTemplate(FileTemplateDescriptor(GRADLE_GITIGNORE_TEMPLATE)) @@ -249,6 +267,21 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory { const val FABRIC_ITEM_TEMPLATE = "FabricItem.java" const val FABRIC_ENCHANTMENT_TEMPLATE = "FabricEnchantment.java" const val FABRIC_STATUS_EFFECT_TEMPLATE = "FabricStatusEffect.java" + + const val NEOFORGE_MIXINS_JSON_TEMPLATE = "NeoForge Mixins Config.json" + const val NEOFORGE_MAIN_CLASS_TEMPLATE = "NeoForge Main Class.java" + const val NEOFORGE_CONFIG_TEMPLATE = "NeoForge Config.java" + const val NEOFORGE_BUILD_GRADLE_TEMPLATE = "NeoForge build.gradle" + const val NEOFORGE_GRADLE_PROPERTIES_TEMPLATE = "NeoForge gradle.properties" + const val NEOFORGE_SETTINGS_GRADLE_TEMPLATE = "NeoForge settings.gradle" + const val NEOFORGE_MODS_TOML_TEMPLATE = "NeoForge mods.toml" + const val NEOFORGE_PACK_MCMETA_TEMPLATE = "NeoForge pack.mcmeta" + + const val NEOFORGE_BLOCK_TEMPLATE = "NeoForgeBlock.java" + const val NEOFORGE_ITEM_TEMPLATE = "NeoForgeItem.java" + const val NEOFORGE_PACKET_TEMPLATE = "NeoForgePacket.java" + const val NEOFORGE_ENCHANTMENT_TEMPLATE = "NeoForgeEnchantment.java" + const val NEOFORGE_MOB_EFFECT_TEMPLATE = "NeoForgeMobEffect.java" } private fun template(fileName: String, displayName: String? = null) = CustomDescriptor(fileName, displayName) diff --git a/src/main/kotlin/util/MinecraftVersions.kt b/src/main/kotlin/util/MinecraftVersions.kt index 5c47f3b15..8635b79e8 100644 --- a/src/main/kotlin/util/MinecraftVersions.kt +++ b/src/main/kotlin/util/MinecraftVersions.kt @@ -36,6 +36,8 @@ object MinecraftVersions { val MC1_19_4 = SemanticVersion.release(1, 19, 4) val MC1_20 = SemanticVersion.release(1, 20) val MC1_20_2 = SemanticVersion.release(1, 20, 2) + val MC1_20_3 = SemanticVersion.release(1, 20, 3) + val MC1_20_4 = SemanticVersion.release(1, 20, 4) fun requiredJavaVersion(minecraftVersion: SemanticVersion) = when { minecraftVersion <= MC1_16_5 -> JavaSdkVersion.JDK_1_8 diff --git a/src/main/kotlin/util/psi-utils.kt b/src/main/kotlin/util/psi-utils.kt index e5608b45d..af3363228 100644 --- a/src/main/kotlin/util/psi-utils.kt +++ b/src/main/kotlin/util/psi-utils.kt @@ -50,6 +50,7 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiModifier import com.intellij.psi.PsiModifier.ModifierConstant +import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiParameter import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiReference @@ -79,6 +80,8 @@ fun PsiElement.findContainingMember(): PsiMember? = findParent(resolveReferences fun PsiElement.findContainingMethod(): PsiMethod? = findParent(resolveReferences = false) { it is PsiClass } +fun PsiElement.findContainingModifierList(): PsiModifierList? = findParent(resolveReferences = false) { it is PsiClass } + private val PsiElement.ancestors: Sequence get() = generateSequence(this) { if (it is PsiFile) null else it.parent } diff --git a/src/main/kotlin/util/reference/ReferenceResolver.kt b/src/main/kotlin/util/reference/ReferenceResolver.kt index da39bfeb7..fc2a8acc7 100644 --- a/src/main/kotlin/util/reference/ReferenceResolver.kt +++ b/src/main/kotlin/util/reference/ReferenceResolver.kt @@ -25,7 +25,7 @@ import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.editor.Editor import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotationMemberValue +import com.intellij.psi.PsiArrayInitializerMemberValue import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiExpression @@ -109,11 +109,10 @@ private fun PsiElement.findContextElement(): PsiElement { var current: PsiElement var parent = this - @Suppress("KotlinConstantConditions") // kotlin is wrong do { current = parent parent = current.parent - if (parent is PsiNameValuePair || parent is PsiAnnotationMemberValue) { + if (parent is PsiNameValuePair || parent is PsiArrayInitializerMemberValue) { return current } } while (parent is PsiExpression) @@ -125,8 +124,11 @@ private fun PsiElement.findContextElement(): PsiElement { * Patches the [LookupElementBuilder] to replace the element with a single * [PsiLiteral] when using code completion. */ -fun LookupElementBuilder.completeToLiteral(context: PsiElement): LookupElementBuilder { - if (context is PsiLiteral) { +fun LookupElementBuilder.completeToLiteral( + context: PsiElement, + extraAction: ((Editor, PsiLiteral) -> Unit)? = null +): LookupElementBuilder { + if (context is PsiLiteral && extraAction == null) { // Context is already a literal return this } @@ -135,7 +137,7 @@ fun LookupElementBuilder.completeToLiteral(context: PsiElement): LookupElementBu // not sure how you would keep line breaks after completion return withInsertHandler { insertionContext, item -> insertionContext.laterRunnable = - ReplaceElementWithLiteral(insertionContext.editor, insertionContext.file, item.lookupString) + ReplaceElementWithLiteral(insertionContext.editor, insertionContext.file, item.lookupString, extraAction) } } @@ -143,6 +145,7 @@ private class ReplaceElementWithLiteral( private val editor: Editor, private val file: PsiFile, private val text: String, + private val extraAction: ((Editor, PsiLiteral) -> Unit)? ) : Runnable { override fun run() { @@ -153,12 +156,16 @@ private class ReplaceElementWithLiteral( CommandProcessor.getInstance().runUndoTransparentAction { runWriteAction { val element = file.findElementAt(editor.caretModel.offset)!!.findContextElement() - element.replace( + val newElement = element.replace( JavaPsiFacade.getElementFactory(element.project).createExpressionFromText( "\"$text\"", element.parent, ), - ) + ) as PsiLiteral + val extraAction = this.extraAction + if (extraAction != null) { + extraAction(editor, newElement) + } } } } diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 8ba206329..b0b44c9fd 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.util import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.intellij.codeInspection.InspectionProfileEntry import com.intellij.lang.java.lexer.JavaLexer import com.intellij.openapi.application.AppUIExecutor import com.intellij.openapi.application.ApplicationManager @@ -42,12 +43,14 @@ import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Ref import com.intellij.openapi.util.text.StringUtil import com.intellij.pom.java.LanguageLevel +import com.intellij.profile.codeInspection.InspectionProfileManager import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import java.lang.invoke.MethodHandles import java.util.Locale import kotlin.math.min import kotlin.reflect.KClass +import org.jetbrains.annotations.NonNls import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.runAsync @@ -131,6 +134,11 @@ fun waitForAllSmart() { } } +inline fun Project.findInspection(@NonNls shortName: String): T? = + InspectionProfileManager.getInstance(this) + .currentProfile.getInspectionTool(shortName, this) + ?.tool as? T + /** * Returns an untyped array for the specified [Collection]. */ @@ -388,3 +396,11 @@ fun S.ifNotBlank(block: (S) -> R): R? { return null } + +inline fun > enumValueOfOrNull(str: String): T? { + return try { + enumValueOf(str) + } catch (e: IllegalArgumentException) { + null + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 1788f30f7..40f8f494f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -41,6 +41,7 @@
  • Paper
  • Sponge
  • Forge
  • +
  • NeoForge
  • Fabric
  • Architectury
  • Velocity
  • @@ -88,6 +89,7 @@ + @@ -108,6 +110,7 @@ + @@ -141,6 +144,7 @@ + @@ -150,6 +154,7 @@ + @@ -165,6 +170,7 @@ * Bukkit * Sponge * Forge + * NeoForge * Fabric * MCP * Mixin @@ -347,6 +353,18 @@ + + + + + + + + + + + + @@ -429,6 +447,8 @@ + @@ -872,7 +892,7 @@ level="ERROR" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.InvalidInjectorMethodSignatureInspection"/> - + + + + @@ -1004,6 +1056,22 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.mixinextras.UnnecessaryMutableLocalInspection"/> + + diff --git a/src/main/resources/assets/icons/platform/NeoForge.png b/src/main/resources/assets/icons/platform/NeoForge.png new file mode 100644 index 000000000..41fe97c87 Binary files /dev/null and b/src/main/resources/assets/icons/platform/NeoForge.png differ diff --git a/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft b/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft index cb6cf20e2..2a1b61ec5 100644 --- a/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft @@ -23,6 +23,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } diff --git a/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft b/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft index a38a3aec8..1be8224dd 100644 --- a/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft @@ -21,6 +21,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } diff --git a/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft b/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft index 9b8508288..be6e106e7 100644 --- a/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft @@ -6,6 +6,10 @@ plugins { version = project.mod_version group = project.maven_group +base { + archivesName = project.archives_base_name +} + repositories { // Add repositories to retrieve artifacts from in here. // You should only use this when depending on other mods because @@ -60,7 +64,6 @@ java { if (JavaVersion.current() < javaVersion) { toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) } - archivesBaseName = project.archives_base_name // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task // if it is present. // If you remove this line, sources will not be generated. diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.ft new file mode 100644 index 000000000..0edac40e6 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.ft @@ -0,0 +1,63 @@ +package ${PACKAGE_NAME}; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.config.ModConfigEvent; +import net.neoforged.neoforge.common.ModConfigSpec; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +// An example config class. This is not required, but it's a good idea to have one to keep your config organized. +// Demonstrates how to use Forge's config APIs +@Mod.EventBusSubscriber(modid = ${CLASS_NAME}.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) +public class Config +{ + private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); + + private static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER + .comment("Whether to log the dirt block on common setup") + .define("logDirtBlock", true); + + private static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER + .comment("A magic number") + .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); + + public static final ModConfigSpec.ConfigValue MAGIC_NUMBER_INTRODUCTION = BUILDER + .comment("What you want the introduction message to be for the magic number") + .define("magicNumberIntroduction", "The magic number is... "); + + // a list of strings that are treated as resource locations for items + private static final ModConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER + .comment("A list of items to log on common setup.") + .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), Config::validateItemName); + + static final ModConfigSpec SPEC = BUILDER.build(); + + public static boolean logDirtBlock; + public static int magicNumber; + public static String magicNumberIntroduction; + public static Set items; + + private static boolean validateItemName(final Object obj) + { + return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(new ResourceLocation(itemName)); + } + + @SubscribeEvent + static void onLoad(final ModConfigEvent event) + { + logDirtBlock = LOG_DIRT_BLOCK.get(); + magicNumber = MAGIC_NUMBER.get(); + magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get(); + + // convert the list of strings into a set of items + items = ITEM_STRINGS.get().stream() + .map(itemName -> BuiltInRegistries.ITEM.get(new ResourceLocation(itemName))) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.html new file mode 100644 index 000000000..2fb3a53e9 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Config.java.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new config class for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.ft new file mode 100644 index 000000000..15b3f4489 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.ft @@ -0,0 +1,129 @@ +package ${PACKAGE_NAME}; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.food.FoodProperties; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; +import net.neoforged.neoforge.event.server.ServerStartingEvent; +import net.neoforged.neoforge.registries.DeferredBlock; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredItem; +import net.neoforged.neoforge.registries.DeferredRegister; +import org.slf4j.Logger; + +// The value here should match an entry in the META-INF/mods.toml file +@Mod(${CLASS_NAME}.MODID) +public class ${CLASS_NAME} +{ + // Define mod id in a common place for everything to reference + public static final String MODID = "${MOD_ID}"; + // Directly reference a slf4j logger + private static final Logger LOGGER = LogUtils.getLogger(); + // Create a Deferred Register to hold Blocks which will all be registered under the "${MOD_ID}" namespace + public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MODID); + // Create a Deferred Register to hold Items which will all be registered under the "${MOD_ID}" namespace + public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID); + // Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "${MOD_ID}" namespace + public static final DeferredRegister CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID); + + // Creates a new Block with the id "${MOD_ID}:example_block", combining the namespace and path + public static final DeferredBlock EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock("example_block", BlockBehaviour.Properties.of().mapColor(MapColor.STONE)); + // Creates a new BlockItem with the id "${MOD_ID}:example_block", combining the namespace and path + public static final DeferredItem EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", EXAMPLE_BLOCK); + + // Creates a new food item with the id "${MOD_ID}:example_id", nutrition 1 and saturation 2 + public static final DeferredItem EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", new Item.Properties().food(new FoodProperties.Builder() + .alwaysEat().nutrition(1).saturationMod(2f).build())); + + // Creates a creative tab with the id "${MOD_ID}:example_tab" for the example item, that is placed after the combat tab + public static final DeferredHolder EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder() + .withTabsBefore(CreativeModeTabs.COMBAT) + .icon(() -> EXAMPLE_ITEM.get().getDefaultInstance()) + .displayItems((parameters, output) -> { + output.accept(EXAMPLE_ITEM.get()); // Add the example item to the tab. For your own tabs, this method is preferred over the event + }).build()); + + // The constructor for the mod class is the first code that is run when your mod is loaded. + // FML will recognize some parameter types like IEventBus or ModContainer and pass them in automatically. + public ${CLASS_NAME}(IEventBus modEventBus) + { + // Register the commonSetup method for modloading + modEventBus.addListener(this::commonSetup); + + // Register the Deferred Register to the mod event bus so blocks get registered + BLOCKS.register(modEventBus); + // Register the Deferred Register to the mod event bus so items get registered + ITEMS.register(modEventBus); + // Register the Deferred Register to the mod event bus so tabs get registered + CREATIVE_MODE_TABS.register(modEventBus); + + // Register ourselves for server and other game events we are interested in + NeoForge.EVENT_BUS.register(this); + + // Register the item to a creative tab + modEventBus.addListener(this::addCreative); + + // Register our mod's ModConfigSpec so that FML can create and load the config file for us + ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC); + } + + private void commonSetup(final FMLCommonSetupEvent event) + { + // Some common setup code + LOGGER.info("HELLO FROM COMMON SETUP"); + + if (Config.logDirtBlock) + LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT)); + + LOGGER.info(Config.magicNumberIntroduction + Config.magicNumber); + + Config.items.forEach((item) -> LOGGER.info("ITEM >> {}", item.toString())); + } + + // Add the example block item to the building blocks tab + private void addCreative(BuildCreativeModeTabContentsEvent event) + { + if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) + event.accept(EXAMPLE_BLOCK_ITEM); + } + + // You can use SubscribeEvent and let the Event Bus discover methods to call + @SubscribeEvent + public void onServerStarting(ServerStartingEvent event) + { + // Do something when the server starts + LOGGER.info("HELLO from server starting"); + } + + // You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent + @Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) + public static class ClientModEvents + { + @SubscribeEvent + public static void onClientSetup(FMLClientSetupEvent event) + { + // Some client setup code + LOGGER.info("HELLO FROM CLIENT SETUP"); + LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName()); + } + } +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.html new file mode 100644 index 000000000..8adaf4200 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new main class for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.ft new file mode 100644 index 000000000..7e2aeffb1 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.ft @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "${PACKAGE_NAME}", + "compatibilityLevel": "JAVA_8", + "refmap": "${MOD_ID}.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.html new file mode 100644 index 000000000..ef90edace --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Mixins Config.json.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new modid.mixins.json file for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft new file mode 100644 index 000000000..9f6eb7544 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft @@ -0,0 +1,141 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.neoforged.gradle.userdev' version '7.0.74' +} + +version = mod_version +group = mod_group_id + +repositories { + mavenLocal() +} + +base { + archivesName = mod_id +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(${JAVA_VERSION}) + +//minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg') +//minecraft.accessTransformers.entry public net.minecraft.client.Minecraft textureManager # textureManager + +// Default run configurations. +// These can be tweaked, removed, or duplicated as needed. +runs { + // applies to all the run configs below + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + systemProperty 'forge.logging.console.level', 'debug' + + modSource project.sourceSets.main + } + + client { + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'forge.enabledGameTestNamespaces', project.mod_id + } + + server { + systemProperty 'forge.enabledGameTestNamespaces', project.mod_id + programArgument '--nogui' + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + systemProperty 'forge.enabledGameTestNamespaces', project.mod_id + } + + data { + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // workingDirectory project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + // Specify the version of Minecraft to use. + // Depending on the plugin applied there are several options. We will assume you applied the userdev plugin as shown above. + // The group for userdev is net.neoforge, the module name is neoforge, and the version is the same as the neoforge version. + // You can however also use the vanilla plugin (net.neoforged.gradle.vanilla) to use a version of Minecraft without the neoforge loader. + // And its provides the option to then use net.minecraft as the group, and one of; client, server or joined as the module name, plus the game version as version. + // For all intends and purposes: You can treat this dependency as if it is a normal library you would use. + implementation "net.neoforged:neoforge:${neo_version}" + + // Example mod dependency with JEI + // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime + // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" + // compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" + // runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" + + // Example mod dependency using a mod jar from ./libs with a flat dir repository + // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar + // The group id is ignored when searching -- in this case, it is "blank" + // implementation "blank:coolmod-${mc_version}:${coolmod_version}" + + // Example mod dependency using a file as dependency + // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") + + // Example project dependency using a sister or child project: + // implementation project(":myproject") + + // For more info: + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [ + minecraft_version : minecraft_version, minecraft_version_range: minecraft_version_range, + neo_version : neo_version, neo_version_range: neo_version_range, + loader_version_range: loader_version_range, + mod_id : mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors : mod_authors, mod_description: mod_description, + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml']) { + expand replaceProperties + [project: project] + } +} + +// Example configuration to allow publishing using the maven-publish plugin +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + } + } + repositories { + maven { + url "file://${project.projectDir}/repo" + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.html new file mode 100644 index 000000000..f7a17d8b0 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new build.gradle for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft new file mode 100644 index 000000000..91afe9ac6 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft @@ -0,0 +1,48 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +org.gradle.jvmargs=-Xmx2G +org.gradle.daemon=false +org.gradle.debug=false + +#[[##]]# Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=${MC_VERSION} +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[${MC_VERSION},${MC_NEXT_VERSION}) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=${NEOFORGE_VERSION} +# The Neo version range can use any version of Neo as bounds +neo_version_range=[${NEOFORGE_SPEC_VERSION},) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[2,) + +#if (${PARCHMENT_VERSION}) +neogradle.subsystems.parchment.minecraftVersion=${PARCHMENT_MC_VERSION} +neogradle.subsystems.parchment.mappingsVersion=${PARCHMENT_VERSION} +#else +# Uncomment this to activate parchment +#neogradle.subsystems.parchment.minecraftVersion=${MC_VERSION} +#neogradle.subsystems.parchment.mappingsVersion=SET_ME +#end + +#[[##]]# Mod Properties + +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=${MOD_ID} +# The human-readable display name for the mod. +mod_name=${MOD_NAME} +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=${LICENSE} +# The mod version. See https://semver.org/ +mod_version=${MOD_VERSION} +# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. +# This should match the base package used for the mod sources. +# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_group_id=${GROUP_ID} +# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_authors=${AUTHOR_LIST} +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=${DESCRIPTION} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.html new file mode 100644 index 000000000..8d45f3608 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new gradle.properties file for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft new file mode 100644 index 000000000..e5439855c --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft @@ -0,0 +1,87 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion="${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId="${mod_id}" #mandatory +# The version number of the mod +version="${mod_version}" #mandatory +# A display name for the mod +displayName="${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#if (${UPDATE_URL}) +updateJSONURL="${UPDATE_URL}" #optional +#else +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +#end +# A URL for the "homepage" for this mod, displayed in the mod UI +#if (${WEBSITE}) +displayURL="${WEBSITE}" #optional +#else +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional +#end +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="${MOD_ID}.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors="${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +#if (${MIXIN_CONFIG}) +[[mixins]] +config="${MIXIN_CONFIG}" +#end + +# The description text for the mod (multi line!) (#mandatory) +description='''${mod_description}''' +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId="neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type="required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange="${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering="NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side="BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId="minecraft" +type="required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange="${minecraft_version_range}" +ordering="NONE" +side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.html new file mode 100644 index 000000000..f0203fe1a --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new mods.toml for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.ft new file mode 100644 index 000000000..7e23528bf --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.ft @@ -0,0 +1,11 @@ +{ + "pack": { + "description": "${MOD_ID} resources", + #if (${PACK_COMMENT} != "") + "pack_format": ${PACK_FORMAT}, + "_comment": "${PACK_COMMENT}" + #else + "pack_format": ${PACK_FORMAT} + #end + } +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.html new file mode 100644 index 000000000..67ae85b9b --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge pack.mcmeta.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new pack.mcmeta for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.ft new file mode 100644 index 000000000..b359a59d3 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.ft @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' +} diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.html new file mode 100644 index 000000000..0705ef41a --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.html @@ -0,0 +1,25 @@ + + + + +

    This is a built-in file template used to create a new settings.gradle for NeoForge projects.

    + + diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft new file mode 100644 index 000000000..f4c8fcbfa --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft @@ -0,0 +1,10 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.level.block.Block; + +public class ${NAME} extends Block { + public ${NAME}(Properties properties) { + super(properties); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html new file mode 100644 index 000000000..e0f3b9c75 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html @@ -0,0 +1,21 @@ + + +A basic block class for NeoForge. diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft new file mode 100644 index 000000000..c4ae0bd57 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft @@ -0,0 +1,12 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentCategory; + +public class ${NAME} extends Enchantment { + public ${NAME}(Rarity rarity, EnchantmentCategory category, EquipmentSlot[] slots) { + super(rarity, category, slots); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html new file mode 100644 index 000000000..9efad589f --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html @@ -0,0 +1,21 @@ + + +NeoForge style enchantment class diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft new file mode 100644 index 000000000..e3ebfd411 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft @@ -0,0 +1,10 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.item.Item; + +public class ${NAME} extends Item { + public ${NAME}(Properties properties) { + super(properties); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html new file mode 100644 index 000000000..8a91b26ef --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html @@ -0,0 +1,21 @@ + + +An empty NeoForge item class. diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft new file mode 100644 index 000000000..49bac7d46 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft @@ -0,0 +1,11 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; + +public class ${NAME} extends MobEffect { + public ${NAME}(MobEffectCategory category, int color) { + super(category, color); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html new file mode 100644 index 000000000..a875fe0ae --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html @@ -0,0 +1,21 @@ + + +NeoForge style mob effect class diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft new file mode 100644 index 000000000..3bbb13ba0 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft @@ -0,0 +1,28 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.network.FriendlyByteBuf; +import net.neoforged.neoforge.network.NetworkEvent; +import java.util.function.Supplier; + +public class ${NAME} { + + public ${NAME}() { + + } + + public ${NAME}(FriendlyByteBuf buf) { + + } + + public void toBytes(FriendlyByteBuf buf) { + + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html new file mode 100644 index 000000000..4fe41befd --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html @@ -0,0 +1,21 @@ + + +NeoForge style packet class diff --git a/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft b/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft index af27f6dc1..500fbecd8 100644 --- a/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft @@ -24,6 +24,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 83d2ec790..44ca652db 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -46,6 +46,11 @@ creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: creator.ui.soft_depend.label=Soft Depend: creator.ui.mixins.label=Use Mixins: +creator.ui.parchment.label=Parchment: +creator.ui.parchment.include.label=Include: +creator.ui.parchment.include.old_mc.label=Older Minecraft versions +creator.ui.parchment.include.snapshots.label=Snapshot versions +creator.ui.parchment.no_version.message=No versions of Parchment matching your configuration creator.ui.outdated.message=Is the Minecraft project wizard outdated? \
    Create an issue on the MinecraftDev issue tracker. @@ -190,3 +195,5 @@ minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter i minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: +minecraft.settings.mixin=Mixin +minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line