diff --git a/build.gradle.kts b/build.gradle.kts
index c4572f82b..d4ac4965b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -83,6 +83,7 @@ repositories {
maven("https://maven.fabricmc.net/") {
content {
includeModule("net.fabricmc", "mapping-io")
+ includeModule("net.fabricmc", "fabric-loader")
}
}
mavenCentral()
@@ -132,6 +133,7 @@ dependencies {
classifier = "shaded"
}
}
+ testLibs(libs.test.fabricloader)
testLibs(libs.test.nbt) {
artifact {
extension = "nbt"
diff --git a/gradle.properties b/gradle.properties
index 1dd79f806..9bebb7a1c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,7 +24,7 @@ kotlin.code.style=official
ideaVersion = 2023.2.2
ideaVersionName = 2023.2.2
-coreVersion = 1.7.4
+coreVersion = 1.7.6
downloadIdeaSources = true
pluginTomlVersion = 232.8660.88
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index abc4aad82..f8dabc6d7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -33,6 +33,7 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio
test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50"
test-mixin = "org.spongepowered:mixin:0.8.5"
test-spongeapi = "org.spongepowered:spongeapi:7.4.0"
+test-fabricloader = "net.fabricmc:fabric-loader:0.15.11"
test-nbt = "com.demonwav.mcdev:all-types-nbt:1.0"
junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
diff --git a/readme.md b/readme.md
index 423f996bb..6d607a566 100644
--- a/readme.md
+++ b/readme.md
@@ -23,6 +23,10 @@ Minecraft Development for IntelliJ
2024.1 |
|
+
+ 2024.2 |
+ |
+
OS Tests |
@@ -31,7 +35,7 @@ Minecraft Development for IntelliJ
|
-Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.7.4-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327)
+Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.7.6-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327)
----------------------
diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy
new file mode 100644
index 000000000..02ef8e305
--- /dev/null
+++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy
@@ -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.mcp.gradle.tooling.neomoddev
+
+import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD
+import org.gradle.api.Project
+import org.jetbrains.annotations.NotNull
+import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder
+import org.jetbrains.plugins.gradle.tooling.ModelBuilderService
+
+import java.nio.file.Files
+
+final class NeoModDevGradleModelBuilderImpl implements ModelBuilderService {
+
+ @Override
+ boolean canBuild(String modelName) {
+ return McpModelNMD.name == modelName
+ }
+
+ @Override
+ Object buildAll(String modelName, Project project) {
+ def extension = project.extensions.findByName('neoForge')
+ if (extension == null) {
+ return null
+ }
+
+ if (!project.plugins.findPlugin("net.neoforged.moddev")) {
+ return null
+ }
+
+ def neoforgeVersion = extension.version.get()
+ if (neoforgeVersion == null) {
+ return null
+ }
+
+ def accessTransformers = extension.accessTransformers.get().collect { project.file(it) }
+
+ // Hacky way to guess where the mappings file is, but I could not find a proper way to find it
+ def neoformDir = project.buildDir.toPath().resolve("neoForm")
+ def mappingsFile = Files.list(neoformDir)
+ .map { it.resolve("config/joined.tsrg") }
+ .filter { Files.exists(it) }
+ .findFirst()
+ .orElse(null)
+ ?.toFile()
+
+ //noinspection GroovyAssignabilityCheck
+ return new NeoModDevGradleModelImpl(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/neomoddev/NeoModDevGradleModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy
new file mode 100644
index 000000000..5640af6fd
--- /dev/null
+++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.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.neomoddev
+
+
+import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD
+import groovy.transform.CompileStatic
+
+@CompileStatic
+final class NeoModDevGradleModelImpl implements McpModelNMD, Serializable {
+
+ final String neoForgeVersion
+ final File mappingsFile
+ final List accessTransformers
+
+ NeoModDevGradleModelImpl(
+ 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/McpModelNMD.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java
new file mode 100644
index 000000000..beff8af28
--- /dev/null
+++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.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 McpModelNMD {
+ 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 382ea8362..9746eaf54 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,6 +1,7 @@
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.neomoddev.NeoModDevGradleModelBuilderImpl
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/creator/buildsystem/gradle-steps.kt b/src/main/kotlin/creator/buildsystem/gradle-steps.kt
index ba7f2d531..8667ae158 100644
--- a/src/main/kotlin/creator/buildsystem/gradle-steps.kt
+++ b/src/main/kotlin/creator/buildsystem/gradle-steps.kt
@@ -61,7 +61,7 @@ import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration
import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject
import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject
-val DEFAULT_GRADLE_VERSION = SemanticVersion.release(8, 5)
+val DEFAULT_GRADLE_VERSION = SemanticVersion.release(8, 7)
val GRADLE_VERSION_KEY = Key.create("mcdev.gradleVersion")
fun FixedAssetsNewProjectWizardStep.addGradleWrapperProperties(project: Project) {
diff --git a/src/main/kotlin/platform/bungeecord/creator/bungee-platforms.kt b/src/main/kotlin/platform/bungeecord/creator/bungee-platforms.kt
index 254c24c49..04b321428 100644
--- a/src/main/kotlin/platform/bungeecord/creator/bungee-platforms.kt
+++ b/src/main/kotlin/platform/bungeecord/creator/bungee-platforms.kt
@@ -28,6 +28,8 @@ import com.demonwav.mcdev.util.SemanticVersion
class BungeeMainPlatformStep(parent: BungeePlatformStep) : AbstractBungeePlatformStep(parent, PlatformType.BUNGEECORD) {
override fun getRepositories(mcVersion: SemanticVersion) = listOf(
BuildRepository("sonatype", "https://oss.sonatype.org/content/groups/public/"),
+ // Seems to be required since 1.21
+ BuildRepository("Minecraft Libraries", "https://libraries.minecraft.net/"),
)
override fun getDependencies(mcVersion: SemanticVersion) = listOf(
diff --git a/src/main/kotlin/platform/fabric/creator/gradle-steps.kt b/src/main/kotlin/platform/fabric/creator/gradle-steps.kt
index c3fe4e8d4..2f4943269 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.5-SNAPSHOT"
+ val loomVersion = "1.6-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/inspection/FabricEntrypointsInspection.kt b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt
index 3ecde42dd..7092fca87 100644
--- a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt
+++ b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt
@@ -22,12 +22,15 @@ package com.demonwav.mcdev.platform.fabric.inspection
import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference
import com.demonwav.mcdev.platform.fabric.util.FabricConstants
+import com.demonwav.mcdev.util.equivalentTo
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.json.psi.JsonArray
import com.intellij.json.psi.JsonElementVisitor
+import com.intellij.json.psi.JsonLiteral
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.psi.JavaPsiFacade
@@ -79,8 +82,7 @@ class FabricEntrypointsInspection : LocalInspectionTool() {
val element = resolved.singleOrNull()?.element
when {
element is PsiClass && !literal.text.contains("::") -> {
- val propertyKey = literal.parentOfType()?.name
- val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] }
+ val (propertyKey, expectedType) = findEntrypointKeyAndType(literal)
if (propertyKey != null && expectedType != null &&
!isEntrypointOfCorrectType(element, propertyKey)
) {
@@ -111,21 +113,43 @@ class FabricEntrypointsInspection : LocalInspectionTool() {
reference.rangeInElement,
)
}
+
+ if (!element.hasModifierProperty(PsiModifier.PUBLIC)) {
+ holder.registerProblem(
+ literal,
+ "Entrypoint method must be public",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ reference.rangeInElement,
+ )
+ }
+
+ if (!element.hasModifierProperty(PsiModifier.STATIC)) {
+ val clazz = element.containingClass
+ if (clazz != null && clazz.constructors.isNotEmpty() &&
+ clazz.constructors.find { !it.hasParameters() } == null
+ ) {
+ holder.registerProblem(
+ literal,
+ "Entrypoint instance method class must have an empty constructor",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ reference.rangeInElement,
+ )
+ }
+ }
}
element is PsiField -> {
- if (!element.hasModifierProperty(PsiModifier.STATIC)) {
+ if (!element.hasModifierProperty(PsiModifier.PUBLIC)) {
holder.registerProblem(
literal,
- "Entrypoint field must be static",
+ "Entrypoint field must be public",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
reference.rangeInElement,
)
}
- val propertyKey = literal.parentOfType()?.name
+ val (propertyKey, expectedType) = findEntrypointKeyAndType(literal)
val fieldTypeClass = (element.type as? PsiClassType)?.resolve()
- val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] }
if (propertyKey != null && fieldTypeClass != null && expectedType != null &&
!isEntrypointOfCorrectType(fieldTypeClass, propertyKey)
) {
@@ -141,11 +165,21 @@ class FabricEntrypointsInspection : LocalInspectionTool() {
}
}
+ private fun findEntrypointKeyAndType(literal: JsonLiteral): Pair {
+ val propertyKey = when (val parent = literal.parent) {
+ is JsonArray -> (parent.parent as? JsonProperty)?.name
+ is JsonProperty -> parent.parentOfType()?.name
+ else -> null
+ }
+ val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] }
+ return propertyKey to expectedType
+ }
+
private fun isEntrypointOfCorrectType(element: PsiClass, type: String): Boolean {
val entrypointClass = FabricConstants.ENTRYPOINT_BY_TYPE[type]
?: return false
val clazz = JavaPsiFacade.getInstance(element.project).findClass(entrypointClass, element.resolveScope)
- return clazz != null && element.isInheritor(clazz, true)
+ return clazz != null && (element.equivalentTo(clazz) || element.isInheritor(clazz, true))
}
}
}
diff --git a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt
index 796058d64..1b8041c8a 100644
--- a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt
+++ b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt
@@ -25,6 +25,8 @@ import com.demonwav.mcdev.util.fullQualifiedName
import com.demonwav.mcdev.util.manipulator
import com.demonwav.mcdev.util.reference.InspectionReference
import com.intellij.codeInsight.completion.JavaLookupElementBuilder
+import com.intellij.json.psi.JsonArray
+import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.openapi.util.TextRange
import com.intellij.psi.JavaPsiFacade
@@ -40,7 +42,9 @@ import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceBase
import com.intellij.psi.PsiReferenceProvider
import com.intellij.psi.ResolveResult
+import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ClassInheritorsSearch
+import com.intellij.psi.util.parentOfType
import com.intellij.util.ArrayUtil
import com.intellij.util.IncorrectOperationException
import com.intellij.util.ProcessingContext
@@ -136,27 +140,17 @@ object EntryPointReference : PsiReferenceProvider() {
fun isEntryPointReference(reference: PsiReference) = reference is Reference
- fun isValidEntrypointClass(element: PsiClass): Boolean {
- val psiFacade = JavaPsiFacade.getInstance(element.project)
- var inheritsEntrypointInterface = false
- for (entrypoint in FabricConstants.ENTRYPOINTS) {
- val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope)
- ?: continue
- if (element.isInheritor(entrypointClass, true)) {
- inheritsEntrypointInterface = true
- break
- }
- }
- return inheritsEntrypointInterface
+ fun isValidEntrypointClass(element: PsiClass, entrypointClass: PsiClass): Boolean {
+ return element.isInheritor(entrypointClass, true)
}
- fun isValidEntrypointField(field: PsiField): Boolean {
+ fun isValidEntrypointField(field: PsiField, entrypointClass: PsiClass): Boolean {
if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC)) {
return false
}
val fieldTypeClass = (field.type as? PsiClassType)?.resolve()
- return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass)
+ return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass, entrypointClass)
}
fun isValidEntrypointMethod(method: PsiMethod): Boolean {
@@ -228,30 +222,36 @@ object EntryPointReference : PsiReferenceProvider() {
val text = element.text.substring(range.startOffset, range.endOffset)
val parts = text.split("::", limit = 2)
+ val psiFacade = JavaPsiFacade.getInstance(element.project)
+ val entrypointType = getEntrypointType()?.let(FabricConstants.ENTRYPOINT_BY_TYPE::get)
+ ?: return ArrayUtil.EMPTY_OBJECT_ARRAY
+ val entrypointClass = psiFacade.findClass(entrypointType, element.resolveScope)
+ ?: return ArrayUtil.EMPTY_OBJECT_ARRAY
+
val variants = mutableListOf()
if (!isMemberReference) {
- val psiFacade = JavaPsiFacade.getInstance(element.project)
- for (entrypoint in FabricConstants.ENTRYPOINTS) {
- val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope)
- ?: continue
- ClassInheritorsSearch.search(entrypointClass, true)
- .mapNotNullTo(variants) {
- val shortName = it.name ?: return@mapNotNullTo null
- val fqName = it.fullQualifiedName ?: return@mapNotNullTo null
- JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName)
- }
- }
+ val scope = element.resolveScope.intersectWith(GlobalSearchScope.projectScope(element.project))
+ ClassInheritorsSearch.search(entrypointClass, scope, true)
+ .mapNotNullTo(variants) {
+ val shortName = it.name ?: return@mapNotNullTo null
+ val fqName = it.fullQualifiedName ?: return@mapNotNullTo null
+ JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName)
+ }
} else if (parts.size >= 2) {
- val psiFacade = JavaPsiFacade.getInstance(element.project)
val className = parts[0].replace('$', '.')
val clazz = psiFacade.findClass(className, element.resolveScope)
if (clazz != null) {
- clazz.fields.filterTo(variants, ::isValidEntrypointField)
+ clazz.fields.filterTo(variants) { isValidEntrypointField(it, entrypointClass) }
clazz.methods.filterTo(variants, ::isValidEntrypointMethod)
}
}
return variants.toTypedArray()
}
+
+ private fun getEntrypointType(): String? {
+ val entrypointsProperty = element.parentOfType()?.parent as? JsonProperty
+ return entrypointsProperty?.name
+ }
}
}
diff --git a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt
index 896da285e..758e3e890 100644
--- a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt
+++ b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt
@@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.fabric.reference
import com.demonwav.mcdev.platform.fabric.util.FabricConstants
import com.demonwav.mcdev.util.isPropertyValue
import com.intellij.json.psi.JsonArray
+import com.intellij.json.psi.JsonElement
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.StandardPatterns
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
@@ -34,19 +36,25 @@ class FabricReferenceContributor : PsiReferenceContributor() {
val stringInModJson = PlatformPatterns.psiElement(JsonStringLiteral::class.java)
.inVirtualFile(PlatformPatterns.virtualFile().withName(FabricConstants.FABRIC_MOD_JSON))
- val entryPointPattern = stringInModJson.withParent(
- PlatformPatterns.psiElement(JsonArray::class.java)
- .withSuperParent(
- 2,
- PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints"),
- ),
- )
+ val entrypointsArray = PlatformPatterns.psiElement(JsonArray::class.java)
+ .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints"))
+ val entryPointSimplePattern = stringInModJson.withParent(entrypointsArray)
+ val entryPointObjectPattern = stringInModJson.isPropertyValue("value")
+ .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).withParent(entrypointsArray))
+ val entryPointPattern = StandardPatterns.or(entryPointSimplePattern, entryPointObjectPattern)
registrar.registerReferenceProvider(entryPointPattern, EntryPointReference)
- val mixinConfigPattern = stringInModJson.withParent(
+ val mixinConfigSimplePattern = stringInModJson.withParent(
PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins"),
)
- registrar.registerReferenceProvider(mixinConfigPattern, ResourceFileReference("mixin config '%s'"))
+ val mixinsConfigArray = PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins")
+ val mixinConfigObjectPattern = stringInModJson.isPropertyValue("config")
+ .withSuperParent(2, PlatformPatterns.psiElement(JsonElement::class.java).withParent(mixinsConfigArray))
+ val mixinConfigPattern = StandardPatterns.or(mixinConfigSimplePattern, mixinConfigObjectPattern)
+ registrar.registerReferenceProvider(
+ mixinConfigPattern,
+ ResourceFileReference("mixin config '%s'", Regex("(.+)\\.mixins\\.json"))
+ )
registrar.registerReferenceProvider(
stringInModJson.isPropertyValue("accessWidener"),
diff --git a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt
index 834ae6e0c..1088b03e9 100644
--- a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt
+++ b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt
@@ -27,8 +27,12 @@ import com.demonwav.mcdev.util.manipulator
import com.demonwav.mcdev.util.mapFirstNotNull
import com.demonwav.mcdev.util.reference.InspectionReference
import com.intellij.json.psi.JsonStringLiteral
+import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.vfs.findPsiFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
@@ -37,13 +41,17 @@ import com.intellij.psi.PsiReferenceBase
import com.intellij.psi.PsiReferenceProvider
import com.intellij.util.IncorrectOperationException
import com.intellij.util.ProcessingContext
+import org.jetbrains.jps.model.java.JavaResourceRootType
-class ResourceFileReference(private val description: String) : PsiReferenceProvider() {
+class ResourceFileReference(
+ private val description: String,
+ private val filenamePattern: Regex? = null
+) : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array {
return arrayOf(Reference(description, element as JsonStringLiteral))
}
- private class Reference(desc: String, element: JsonStringLiteral) :
+ private inner class Reference(desc: String, element: JsonStringLiteral) :
PsiReferenceBase(element),
InspectionReference {
override val description = desc
@@ -61,6 +69,9 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi
?: ModuleRootManager.getInstance(module)
.getDependencies(false)
.mapFirstNotNull(::findFileIn)
+ ?: ModuleManager.getInstance(element.project)
+ .getModuleDependentModules(module)
+ .mapFirstNotNull(::findFileIn)
}
override fun bindToElement(newTarget: PsiElement): PsiElement? {
@@ -70,5 +81,30 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi
val manipulator = element.manipulator ?: return null
return manipulator.handleContentChange(element, manipulator.getRangeInElement(element), newTarget.name)
}
+
+ override fun getVariants(): Array {
+ if (filenamePattern == null) {
+ return emptyArray()
+ }
+
+ val module = element.findModule() ?: return emptyArray()
+ val variants = mutableListOf()
+ val relevantModules = ModuleManager.getInstance(element.project).getModuleDependentModules(module) + module
+ runReadAction {
+ val relevantRoots = relevantModules.flatMap {
+ it.rootManager.getSourceRoots(JavaResourceRootType.RESOURCE)
+ }
+ for (roots in relevantRoots) {
+ for (child in roots.children) {
+ val relativePath = child.path.removePrefix(roots.path)
+ val testRelativePath = "/$relativePath"
+ if (testRelativePath.matches(filenamePattern)) {
+ variants.add(child.findPsiFile(element.project) ?: relativePath)
+ }
+ }
+ }
+ }
+ return variants.toTypedArray()
+ }
}
}
diff --git a/src/main/kotlin/platform/forge/creator/asset-steps.kt b/src/main/kotlin/platform/forge/creator/asset-steps.kt
index a52811858..e68058f8f 100644
--- a/src/main/kotlin/platform/forge/creator/asset-steps.kt
+++ b/src/main/kotlin/platform/forge/creator/asset-steps.kt
@@ -108,6 +108,7 @@ class ForgeProjectFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningA
}
val mainClassTemplate = when {
+ mcVersion >= MinecraftVersions.MC1_20_6 -> MinecraftTemplates.FG3_1_20_6_MAIN_CLASS_TEMPLATE
mcVersion >= MinecraftVersions.MC1_20 -> MinecraftTemplates.FG3_1_20_MAIN_CLASS_TEMPLATE
mcVersion >= MinecraftVersions.MC1_19_3 -> MinecraftTemplates.FG3_1_19_3_MAIN_CLASS_TEMPLATE
mcVersion >= MinecraftVersions.MC1_19 -> MinecraftTemplates.FG3_1_19_MAIN_CLASS_TEMPLATE
@@ -124,6 +125,7 @@ class ForgeProjectFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningA
)
val configTemplate = when {
+ mcVersion >= MinecraftVersions.MC1_21 -> MinecraftTemplates.FG3_1_21_CONFIG_TEMPLATE
mcVersion >= MinecraftVersions.MC1_20 -> MinecraftTemplates.FG3_1_20_CONFIG_TEMPLATE
else -> null
}
diff --git a/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt b/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt
index 4678979d5..9fb68a20a 100644
--- a/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt
+++ b/src/main/kotlin/platform/forge/util/ForgePackDescriptor.kt
@@ -55,6 +55,8 @@ data class ForgePackDescriptor(val format: Int, val comment: String) {
val FORMAT_15 = ForgePackDescriptor(15, "")
val FORMAT_18 = ForgePackDescriptor(18, "")
val FORMAT_26 = ForgePackDescriptor(26, "")
+ val FORMAT_41 = ForgePackDescriptor(41, "")
+ val FORMAT_48 = ForgePackDescriptor(48, "")
// See https://minecraft.gamepedia.com/Tutorials/Creating_a_resource_pack#.22pack_format.22
fun forMcVersion(version: SemanticVersion): ForgePackDescriptor? = when {
@@ -69,7 +71,9 @@ data class ForgePackDescriptor(val format: Int, val comment: String) {
version < MinecraftVersions.MC1_20 -> FORMAT_12
version < MinecraftVersions.MC1_20_2 -> FORMAT_15
version < MinecraftVersions.MC1_20_3 -> FORMAT_18
- version >= MinecraftVersions.MC1_20_3 -> FORMAT_26
+ version < MinecraftVersions.MC1_20_5 -> FORMAT_26
+ version < MinecraftVersions.MC1_21 -> FORMAT_41
+ version >= MinecraftVersions.MC1_21 -> FORMAT_48
else -> null
}
}
diff --git a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt
index 53b90142a..5d45a3d79 100644
--- a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt
+++ b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt
@@ -23,9 +23,11 @@ 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.datahandler.McpModelNMDHandler
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.platform.mcp.gradle.tooling.McpModelNMD
import com.demonwav.mcdev.util.runGradleTask
import com.intellij.openapi.externalSystem.model.DataNode
import com.intellij.openapi.externalSystem.model.project.ModuleData
@@ -38,7 +40,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, McpModelNG7::class.java)
+ setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java, McpModelNMD::class.java)
override fun getToolingExtensionsClasses() = extraProjectModelClasses
@@ -91,6 +93,6 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() {
}
companion object {
- private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler)
+ private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler, McpModelNMDHandler)
}
}
diff --git a/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt
new file mode 100644
index 000000000..ca496ad49
--- /dev/null
+++ b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.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.McpModelNMD
+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 McpModelNMDHandler : McpModelDataHandler {
+
+ override fun build(
+ gradleModule: IdeaModule,
+ node: DataNode,
+ resolverCtx: ProjectResolverContext,
+ ) {
+ val data = resolverCtx.getExtraProject(gradleModule, McpModelNMD::class.java) ?: return
+
+ val state = McpModuleSettings.State(
+ "1." + data.neoForgeVersion.substringBefore('.'),
+ 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/handlers/InjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
index 798eef15c..69384ea99 100644
--- a/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/InjectorAnnotationHandler.kt
@@ -55,19 +55,25 @@ import org.objectweb.asm.tree.MethodNode
abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
override fun resolveTarget(annotation: PsiAnnotation, targetClass: ClassNode): List {
- val targetClassMethods = targetClass.methods ?: return emptyList()
-
val methodAttr = annotation.findAttributeValue("method")
val method = methodAttr?.computeStringArray() ?: emptyList()
val desc = annotation.findAttributeValue("desc")?.findAnnotations() ?: emptyList()
val selectors = method.mapNotNull { parseMixinSelector(it, methodAttr!!) } +
desc.mapNotNull { DescSelectorParser.descSelectorFromAnnotation(it) }
- return targetClassMethods.mapNotNull { targetMethod ->
- if (selectors.any { it.matchMethod(targetMethod, targetClass) }) {
- MethodTargetMember(targetClass, targetMethod)
- } else {
- null
+ val targetClassMethods = selectors.associateWith { selector ->
+ val actualTarget = selector.getCustomOwner(targetClass)
+ (actualTarget to actualTarget.methods)
+ }
+
+ return targetClassMethods.mapNotNull { (selector, pair) ->
+ val (clazz, methods) = pair
+ methods.firstNotNullOfOrNull { method ->
+ if (selector.matchMethod(method, clazz)) {
+ MethodTargetMember(clazz, method)
+ } else {
+ null
+ }
}
}
}
@@ -100,7 +106,7 @@ abstract class InjectorAnnotationHandler : MixinAnnotationHandler {
override fun resolveForNavigation(annotation: PsiAnnotation, targetClass: ClassNode): List {
return resolveTarget(annotation, targetClass).flatMap { targetMember ->
val targetMethod = targetMember as? MethodTargetMember ?: return@flatMap emptyList()
- resolveForNavigation(annotation, targetClass, targetMethod.classAndMethod.method)
+ resolveForNavigation(annotation, targetMethod.classAndMethod.clazz, targetMethod.classAndMethod.method)
}
}
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
index 2f4a5596b..ec61f4a4c 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
@@ -20,6 +20,7 @@
package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint
+import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
import com.demonwav.mcdev.platform.mixin.reference.isMiscDynamicSelector
import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector
import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference
@@ -152,7 +153,7 @@ class AtResolver(
val collectVisitor = injectionPoint.createCollectVisitor(
at,
target,
- targetClass,
+ getTargetClass(target),
CollectVisitor.Mode.MATCH_FIRST,
)
if (collectVisitor == null) {
@@ -181,7 +182,7 @@ class AtResolver(
val targetAttr = at.findAttributeValue("target")
val target = targetAttr?.let { parseMixinSelector(it) }
- val collectVisitor = injectionPoint.createCollectVisitor(at, target, targetClass, mode)
+ val collectVisitor = injectionPoint.createCollectVisitor(at, target, getTargetClass(target), mode)
?: return InsnResolutionInfo.Failure()
collectVisitor.visit(targetMethod)
val result = collectVisitor.result
@@ -201,7 +202,7 @@ class AtResolver(
// Then attempt to find the corresponding source elements using the navigation visitor
val targetElement = targetMethod.findSourceElement(
- targetClass,
+ getTargetClass(target),
at.project,
GlobalSearchScope.allScope(at.project),
canDecompile = true,
@@ -224,16 +225,23 @@ class AtResolver(
// Collect all possible targets
fun doCollectVariants(injectionPoint: InjectionPoint): List {
- val visitor = injectionPoint.createCollectVisitor(at, target, targetClass, CollectVisitor.Mode.COMPLETION)
+ val visitor = injectionPoint.createCollectVisitor(
+ at, target, getTargetClass(target),
+ CollectVisitor.Mode.COMPLETION
+ )
?: return emptyList()
visitor.visit(targetMethod)
return visitor.result
.mapNotNull { result ->
- injectionPoint.createLookup(targetClass, result)?.let { completionHandler(it) }
+ injectionPoint.createLookup(getTargetClass(target), result)?.let { completionHandler(it) }
}
}
return doCollectVariants(injectionPoint)
}
+
+ private fun getTargetClass(selector: MixinSelector?): ClassNode {
+ return selector?.getCustomOwner(targetClass) ?: targetClass
+ }
}
sealed class InsnResolutionInfo {
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
new file mode 100644
index 000000000..938f0103e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.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.handlers.mixinextras
+
+import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InsnResolutionInfo
+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.findSourceElement
+import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
+import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
+import com.demonwav.mcdev.util.Parameter
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.search.GlobalSearchScope
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
+
+class WrapMethodHandler : InjectorAnnotationHandler() {
+ override fun expectedMethodSignature(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ ): List {
+ val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
+
+ return listOf(
+ MethodSignature(
+ listOf(
+ ParameterGroup(
+ collectTargetMethodParameters(annotation.project, targetClass, targetMethod) +
+ Parameter(
+ "original",
+ mixinExtrasOperationType(annotation, returnType) ?: return emptyList()
+ ),
+ )
+ ),
+ returnType
+ )
+ )
+ }
+
+ override fun isUnresolved(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode
+ ): InsnResolutionInfo.Failure? {
+ // If we've got a target method that's good enough
+ return null
+ }
+
+ override fun resolveForNavigation(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode
+ ): List {
+ val project = annotation.project
+ return targetMethod.findSourceElement(
+ targetClass,
+ project,
+ GlobalSearchScope.allScope(project),
+ canDecompile = true
+ )?.let(::listOf).orEmpty()
+ }
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
+}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
index bf1bcbb30..2acb6be78 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
@@ -22,13 +22,11 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
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.MixinConstants.MixinExtras.OPERATION
+import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
-import com.intellij.psi.PsiElement
-import com.intellij.psi.PsiPrimitiveType
import com.intellij.psi.PsiType
import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import com.llamalad7.mixinextras.utils.Decorations
@@ -55,7 +53,7 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() {
): Pair? {
val params = getParameterTypes(target, targetClass, annotation) ?: return null
val returnType = getReturnType(target, annotation) ?: return null
- val operationType = getOperationType(annotation, returnType) ?: return null
+ val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null
return ParameterGroup(
params + Parameter("original", operationType)
) to returnType
@@ -91,17 +89,5 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() {
return type.toPsiType(JavaPsiFacade.getElementFactory(annotation.project))
}
- private fun getOperationType(context: PsiElement, type: PsiType): PsiType? {
- val project = context.project
- val boxedType = if (type is PsiPrimitiveType) {
- type.getBoxedType(context) ?: return null
- } else {
- type
- }
-
- return JavaPsiFacade.getElementFactory(project)
- .createTypeFromText("$OPERATION<${boxedType.canonicalText}>", context)
- }
-
override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_OPERATION
}
diff --git a/src/main/kotlin/platform/mixin/inspection/MixinClassReferenceInspection.kt b/src/main/kotlin/platform/mixin/inspection/MixinClassReferenceInspection.kt
index 43ae79ea9..4d91abbf3 100644
--- a/src/main/kotlin/platform/mixin/inspection/MixinClassReferenceInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/MixinClassReferenceInspection.kt
@@ -20,14 +20,17 @@
package com.demonwav.mcdev.platform.mixin.inspection
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin
import com.demonwav.mcdev.platform.mixin.util.isMixin
import com.demonwav.mcdev.util.findContainingClass
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.JavaElementVisitor
+import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiTypeElement
+import com.intellij.psi.util.parentOfType
class MixinClassReferenceInspection : MixinInspection() {
@@ -58,6 +61,11 @@ class MixinClassReferenceInspection : MixinInspection() {
return
}
+ val annotation = type.parentOfType()
+ if (annotation != null && annotation.hasQualifiedName(MixinConstants.Annotations.DYNAMIC)) {
+ return
+ }
+
holder.registerProblem(type, "Mixin class cannot be referenced directly")
}
}
diff --git a/src/main/kotlin/platform/mixin/inspection/StaticMemberInspection.kt b/src/main/kotlin/platform/mixin/inspection/StaticMemberInspection.kt
index a1395319d..f747cdb57 100644
--- a/src/main/kotlin/platform/mixin/inspection/StaticMemberInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/StaticMemberInspection.kt
@@ -37,7 +37,7 @@ import com.intellij.psi.PsiModifier
class StaticMemberInspection : MixinInspection() {
override fun getStaticDescription() =
- "A mixin class does not exist at runtime, and thus having them public does not make sense. " +
+ "A mixin class does not exist at runtime, and thus having them not private does not make sense. " +
"Make the field/method private instead."
override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder)
@@ -56,7 +56,7 @@ class StaticMemberInspection : MixinInspection() {
if (isProblematic(member)) {
holder.registerProblem(
member,
- "Public static members are not allowed in Mixin classes",
+ "Non-private static members are not allowed in Mixin classes",
QuickFixFactory.getInstance().createModifierListFix(member, PsiModifier.PRIVATE, true, false),
)
}
@@ -70,7 +70,7 @@ class StaticMemberInspection : MixinInspection() {
val modifiers = member.modifierList!!
- return modifiers.hasModifierProperty(PsiModifier.PUBLIC) &&
+ return !modifiers.hasModifierProperty(PsiModifier.PRIVATE) &&
modifiers.hasModifierProperty(PsiModifier.STATIC) &&
modifiers.findAnnotation(SHADOW) == null &&
modifiers.findAnnotation(OVERWRITE) == null &&
diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/WrongOperationParametersInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/WrongOperationParametersInspection.kt
index 34491a9fa..12416ea77 100644
--- a/src/main/kotlin/platform/mixin/inspection/mixinextras/WrongOperationParametersInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/WrongOperationParametersInspection.kt
@@ -61,7 +61,9 @@ class WrongOperationParametersInspection : MixinInspection() {
PsiField::class.java
) ?: return
- if (!containingMethod.hasAnnotation(MixinConstants.MixinExtras.WRAP_OPERATION)) {
+ if (!containingMethod.hasAnnotation(MixinConstants.MixinExtras.WRAP_OPERATION) &&
+ !containingMethod.hasAnnotation(MixinConstants.MixinExtras.WRAP_METHOD)
+ ) {
return
}
diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt
index 67b355f30..89aa5dbd5 100644
--- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt
+++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt
@@ -83,7 +83,9 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference
val stringValue = context.constantStringValue ?: return false
val targetMethodInfo = parseSelector(stringValue, context) ?: return false
val targets = getTargets(context) ?: return false
- return !targets.asSequence().flatMap { it.findMethods(targetMethodInfo) }.any()
+ return !targets.asSequence().flatMap {
+ targetMethodInfo.getCustomOwner(it).findMethods(targetMethodInfo)
+ }.any()
}
fun getReferenceIfAmbiguous(context: PsiElement): MemberReference? {
@@ -125,7 +127,10 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference
selector: MixinSelector,
): Sequence {
return targets.asSequence()
- .flatMap { target -> target.findMethods(selector).map { ClassAndMethodNode(target, it) } }
+ .flatMap { target ->
+ val actualTarget = selector.getCustomOwner(target)
+ actualTarget.findMethods(selector).map { ClassAndMethodNode(actualTarget, it) }
+ }
}
fun resolveIfUnique(context: PsiElement): ClassAndMethodNode? {
diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
index 18e7c4b1b..88e912674 100644
--- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
+++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
@@ -51,6 +51,7 @@ import com.intellij.openapi.util.RecursionManager
import com.intellij.psi.CommonClassNames
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiCallExpression
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiField
@@ -60,6 +61,7 @@ import com.intellij.psi.PsiNameValuePair
import com.intellij.psi.PsiTypes
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.AnnotatedMembersSearch
+import com.intellij.psi.search.searches.MethodReferencesSearch
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.psi.util.PsiTreeUtil
@@ -122,6 +124,10 @@ interface MixinSelector {
return matchMethod(qualifier.name, method.name, method.desc)
}
+ fun getCustomOwner(owner: ClassNode): ClassNode {
+ return owner
+ }
+
/**
* Implement this to return false for early-out optimizations, so you don't need to resolve the member in the
* navigation visitor
@@ -352,13 +358,18 @@ private fun getAllDynamicSelectors(project: Project): Set {
val annotation = member.findAnnotation(MixinConstants.Classes.SELECTOR_ID) ?: return@flatMap emptySequence()
val value = annotation.findAttributeValue("value")?.constantStringValue
?: return@flatMap emptySequence()
- val namespace = annotation.findAttributeValue("namespace")?.constantStringValue
+ var namespace = annotation.findAttributeValue("namespace")?.constantStringValue
if (namespace.isNullOrEmpty()) {
val builtinPrefix = "org.spongepowered.asm.mixin.injection.selectors."
if (member.qualifiedName?.startsWith(builtinPrefix) == true) {
sequenceOf(value, "mixin:$value")
} else {
- sequenceOf(value)
+ namespace = findNamespace(project, member)
+ if (namespace != null) {
+ sequenceOf("$namespace:$value")
+ } else {
+ sequenceOf(value)
+ }
}
} else {
sequenceOf("$namespace:$value")
@@ -367,6 +378,38 @@ private fun getAllDynamicSelectors(project: Project): Set {
}
}
+/**
+ * Dynamic selectors don't have to declare their namespace in the annotation,
+ * so instead we look for the registration call and extract the namespace from there.
+ */
+private fun findNamespace(
+ project: Project,
+ member: PsiClass
+): String? {
+ val targetSelector = JavaPsiFacade.getInstance(project)
+ .findClass(MixinConstants.Classes.TARGET_SELECTOR, GlobalSearchScope.allScope(project))
+ val registerMethod = targetSelector?.findMethodsByName("register", false)?.firstOrNull() ?: return null
+
+ val query = MethodReferencesSearch.search(registerMethod)
+ val usages = query.findAll()
+ for (usage in usages) {
+ val element = usage.element
+ val callExpression = PsiTreeUtil.getParentOfType(element, PsiCallExpression::class.java) ?: continue
+ val args = callExpression.argumentList ?: continue
+ if (args.expressions.size != 2) continue
+
+ // is the registered selector the one we're checking?
+ val selectorName = args.expressions[0].text.removeSuffix(".class")
+ if (selectorName != member.name) continue
+
+ val namespaceArg = args.expressions[1].text.removeSurrounding("\"")
+ if (namespaceArg.isEmpty()) continue
+
+ return namespaceArg
+ }
+ return null
+}
+
private val DYNAMIC_SELECTOR_PATTERN = "(?i)^@([a-z]+(:[a-z]+)?)(\\((.*)\\))?$".toRegex()
abstract class DynamicSelectorParser(id: String, vararg aliases: String) : MixinSelectorParser {
diff --git a/src/main/kotlin/platform/mixin/util/Mixin.kt b/src/main/kotlin/platform/mixin/util/Mixin.kt
index b7bd17629..844ee1433 100644
--- a/src/main/kotlin/platform/mixin/util/Mixin.kt
+++ b/src/main/kotlin/platform/mixin/util/Mixin.kt
@@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.INVOKER
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MIXIN
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.CALLBACK_INFO_RETURNABLE
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants.MixinExtras.OPERATION
import com.demonwav.mcdev.util.cached
import com.demonwav.mcdev.util.computeStringArray
import com.demonwav.mcdev.util.findModule
@@ -154,6 +155,18 @@ fun callbackInfoReturnableType(project: Project, context: PsiElement, returnType
.createTypeFromText("$CALLBACK_INFO_RETURNABLE<${boxedType.canonicalText}>", context)
}
+fun mixinExtrasOperationType(context: PsiElement, type: PsiType): PsiType? {
+ val project = context.project
+ val boxedType = if (type is PsiPrimitiveType) {
+ type.getBoxedType(context) ?: return null
+ } else {
+ type
+ }
+
+ return JavaPsiFacade.getElementFactory(project)
+ .createTypeFromText("$OPERATION<${boxedType.canonicalText}>", context)
+}
+
fun isAssignable(left: PsiType, right: PsiType, allowPrimitiveConversion: Boolean = true): Boolean {
return when {
left is PsiIntersectionType -> left.conjuncts.all { isAssignable(it, right) }
diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt
index c7503a070..67f145214 100644
--- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt
+++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt
@@ -38,6 +38,7 @@ object MixinConstants {
const val CONSTANT_CONDITION = "org.spongepowered.asm.mixin.injection.Constant.Condition"
const val INJECTION_POINT = "org.spongepowered.asm.mixin.injection.InjectionPoint"
const val SELECTOR = "org.spongepowered.asm.mixin.injection.InjectionPoint.Selector"
+ const val TARGET_SELECTOR = "org.spongepowered.asm.mixin.injection.selectors.TargetSelector"
const val MIXIN_AGENT = "org.spongepowered.tools.agent.MixinAgent"
const val MIXIN_CONFIG = "org.spongepowered.asm.mixin.transformer.MixinConfig"
const val MIXIN_PLUGIN = "org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin"
@@ -85,6 +86,7 @@ object MixinConstants {
object MixinExtras {
const val OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.Operation"
const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation"
+ const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod"
const val LOCAL = "com.llamalad7.mixinextras.sugar.Local"
const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref."
const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression"
diff --git a/src/main/kotlin/platform/neoforge/creator/asset-steps.kt b/src/main/kotlin/platform/neoforge/creator/asset-steps.kt
index 64cd30844..1d7af10e6 100644
--- a/src/main/kotlin/platform/neoforge/creator/asset-steps.kt
+++ b/src/main/kotlin/platform/neoforge/creator/asset-steps.kt
@@ -41,6 +41,7 @@ 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.MinecraftVersions
import com.demonwav.mcdev.util.SemanticVersion
import com.intellij.ide.wizard.NewProjectWizardStep
import com.intellij.openapi.project.Project
@@ -101,21 +102,37 @@ class NeoForgeProjectFilesStep(parent: NewProjectWizardStep) : AbstractLongRunni
assets.addTemplateProperties("WEBSITE" to website)
}
- val mainClassTemplate = MinecraftTemplates.NEOFORGE_MAIN_CLASS_TEMPLATE
+ val mainClassTemplate = when {
+ mcVersion >= MinecraftVersions.MC1_20_5 -> MinecraftTemplates.NEOFORGE_1_20_5_MAIN_CLASS_TEMPLATE
+ else -> MinecraftTemplates.NEOFORGE_MAIN_CLASS_TEMPLATE
+ }
+
+ val (modManifestName, modManifestTemplate) = when {
+ mcVersion >= MinecraftVersions.MC1_20_5 ->
+ "neoforge.mods.toml" to MinecraftTemplates.NEOFORGE_NEOFORGE_MODS_TOML_TEMPLATE
+
+ else -> "mods.toml" to MinecraftTemplates.NEOFORGE_MODS_TOML_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,
+ "src/main/resources/META-INF/$modManifestName" to modManifestTemplate,
)
+ val configClassTemplate = when {
+ mcVersion >= MinecraftVersions.MC1_21 -> MinecraftTemplates.NEOFORGE_1_21_CONFIG_TEMPLATE
+ mcVersion >= MinecraftVersions.MC1_20_5 -> MinecraftTemplates.NEOFORGE_1_20_5_CONFIG_TEMPLATE
+ else -> MinecraftTemplates.NEOFORGE_CONFIG_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.addTemplates(project, configPath to configClassTemplate)
assets.addLicense(project)
}
diff --git a/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt b/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt
index f64e676be..e1592551e 100644
--- a/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt
+++ b/src/main/kotlin/platform/neoforge/creator/gradle-steps.kt
@@ -41,6 +41,7 @@ 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.MinecraftVersions
import com.demonwav.mcdev.util.SemanticVersion
import com.intellij.ide.wizard.NewProjectWizardStep
import com.intellij.openapi.application.WriteAction
@@ -49,7 +50,7 @@ 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)
+private val ngWrapperVersion = SemanticVersion.release(8, 7)
const val MAGIC_RUN_CONFIGS_FILE = ".hello_from_mcdev"
@@ -71,6 +72,7 @@ class NeoForgeGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunnin
override fun setupAssets(project: Project) {
val mcVersion = data.getUserData(NeoForgeVersionChainStep.MC_VERSION_KEY) ?: return
val neoforgeVersion = data.getUserData(NeoForgeVersionChainStep.NEOFORGE_VERSION_KEY) ?: return
+ val neogradleVersion = data.getUserData(NeoForgeVersionChainStep.NEOGRADLE_VERSION_KEY) ?: return
val modId = data.getUserData(AbstractModIdStep.KEY) ?: return
val modName = data.getUserData(AbstractModNameStep.KEY) ?: return
val buildSystemProps = findStep>()
@@ -85,6 +87,10 @@ class NeoForgeGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunnin
} else {
mcVersion
}
+ val loaderVersion = when {
+ mcVersion < MinecraftVersions.MC1_21 -> "2"
+ else -> "4"
+ }
data.putUserData(GRADLE_VERSION_KEY, ngWrapperVersion)
@@ -95,6 +101,8 @@ class NeoForgeGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunnin
"MC_NEXT_VERSION" to mcNextVersion,
"NEOFORGE_VERSION" to neoforgeVersion,
"NEOFORGE_SPEC_VERSION" to neoforgeVersion.parts[0].versionString,
+ "LOADER_VERSION" to loaderVersion,
+ "NEOGRADLE_VERSION" to neogradleVersion,
"GROUP_ID" to buildSystemProps.groupId,
"ARTIFACT_ID" to buildSystemProps.artifactId,
"MOD_VERSION" to buildSystemProps.version,
@@ -115,13 +123,14 @@ class NeoForgeGradleFilesStep(parent: NewProjectWizardStep) : AbstractLongRunnin
assets.addTemplateProperties("JAVA_VERSION" to javaVersion.feature)
}
- if (neoforgeVersion >= SemanticVersion.release(39, 0, 88)) {
- assets.addTemplateProperties("GAME_TEST_FRAMEWORK" to "true")
+ val buildGradleTemplate = when {
+ mcVersion >= MinecraftVersions.MC1_20_5 -> MinecraftTemplates.NEOFORGE_1_20_5_BUILD_GRADLE_TEMPLATE
+ else -> MinecraftTemplates.NEOFORGE_BUILD_GRADLE_TEMPLATE
}
assets.addTemplates(
project,
- "build.gradle" to MinecraftTemplates.NEOFORGE_BUILD_GRADLE_TEMPLATE,
+ "build.gradle" to buildGradleTemplate,
"gradle.properties" to MinecraftTemplates.NEOFORGE_GRADLE_PROPERTIES_TEMPLATE,
"settings.gradle" to MinecraftTemplates.NEOFORGE_SETTINGS_GRADLE_TEMPLATE,
)
diff --git a/src/main/kotlin/platform/neoforge/creator/ui-steps.kt b/src/main/kotlin/platform/neoforge/creator/ui-steps.kt
index 86755fed2..8a0d03909 100644
--- a/src/main/kotlin/platform/neoforge/creator/ui-steps.kt
+++ b/src/main/kotlin/platform/neoforge/creator/ui-steps.kt
@@ -36,6 +36,7 @@ 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.platform.neoforge.version.NeoGradleVersion
import com.demonwav.mcdev.util.MinecraftVersions
import com.demonwav.mcdev.util.SemanticVersion
import com.demonwav.mcdev.util.asyncIO
@@ -47,25 +48,31 @@ import kotlinx.coroutines.coroutineScope
private val minSupportedMcVersion = MinecraftVersions.MC1_20_2
-class NeoForgePlatformStep(parent: ModPlatformStep) : AbstractLatentStep(parent) {
+class NeoForgePlatformStep(parent: ModPlatformStep) :
+ AbstractLatentStep>(parent) {
override val description = "fetch NeoForge versions"
override suspend fun computeData() = coroutineScope {
- asyncIO { NeoForgeVersion.downloadData() }.await()
+ val neoforgeJob = asyncIO { NeoForgeVersion.downloadData() }
+ val neogradleJob = asyncIO { NeoGradleVersion.downloadData() }
+ val neoforge = neoforgeJob.await() ?: return@coroutineScope null
+ val neogradle = neogradleJob.await() ?: return@coroutineScope null
+ neoforge to neogradle
}
- 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)
+ override fun createStep(data: Pair): NewProjectWizardStep =
+ NeoForgeVersionChainStep(this, data.first, data.second)
+ .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"
@@ -76,13 +83,17 @@ class NeoForgePlatformStep(parent: ModPlatformStep) : AbstractLatentStep("${NeoForgeVersionChainStep::class.java}.mcVersion")
val NEOFORGE_VERSION_KEY =
Key.create("${NeoForgeVersionChainStep::class.java}.neoforgeVersion")
+ val NEOGRADLE_VERSION_KEY =
+ Key.create("${NeoForgeVersionChainStep::class.java}.neogradleVersion")
}
override fun getAvailableVersions(versionsAbove: List>): List> {
@@ -90,6 +101,8 @@ class NeoForgeVersionChainStep(
MINECRAFT_VERSION -> neoforgeVersionData.sortedMcVersions.filter { it >= minSupportedMcVersion }
NEOFORGE_VERSION ->
neoforgeVersionData.getNeoForgeVersions(versionsAbove[MINECRAFT_VERSION] as SemanticVersion)
+
+ NEOGRADLE_VERSION -> neogradleVersionData.versions
else -> throw IncorrectOperationException()
}
}
@@ -98,6 +111,7 @@ class NeoForgeVersionChainStep(
super.setupProject(project)
data.putUserData(MC_VERSION_KEY, getVersion(MINECRAFT_VERSION) as SemanticVersion)
data.putUserData(NEOFORGE_VERSION_KEY, getVersion(NEOFORGE_VERSION) as SemanticVersion)
+ data.putUserData(NEOGRADLE_VERSION_KEY, getVersion(NEOGRADLE_VERSION) as SemanticVersion)
}
}
diff --git a/src/main/kotlin/platform/neoforge/version/NeoGradleVersion.kt b/src/main/kotlin/platform/neoforge/version/NeoGradleVersion.kt
new file mode 100644
index 000000000..b7abd0aba
--- /dev/null
+++ b/src/main/kotlin/platform/neoforge/version/NeoGradleVersion.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.version
+
+import com.demonwav.mcdev.creator.collectMavenVersions
+import com.demonwav.mcdev.util.SemanticVersion
+import com.intellij.openapi.diagnostic.logger
+import java.io.IOException
+
+class NeoGradleVersion private constructor(val versions: List) {
+
+ companion object {
+ private val LOGGER = logger()
+
+ suspend fun downloadData(): NeoGradleVersion? {
+ try {
+ val url = "https://maven.neoforged.net/releases/net/neoforged/gradle/userdev/maven-metadata.xml"
+ val versions = collectMavenVersions(url)
+ .asSequence()
+ .mapNotNull(SemanticVersion.Companion::tryParse)
+ .sortedDescending()
+ .take(50)
+ .toList()
+ return NeoGradleVersion(versions)
+ } catch (e: IOException) {
+ LOGGER.error("Failed to retrieve NeoForge version data", e)
+ }
+ return null
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt
index 7387d5d5a..76c14eb02 100644
--- a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt
+++ b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt
@@ -244,11 +244,13 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
)
}
}
- "ninja.leaping.configurate.loader.ConfigurationLoader" -> {
+ "ninja.leaping.configurate.loader.ConfigurationLoader",
+ "org.spongepowered.configurate.reference.ConfigurationReference",
+ "org.spongepowered.configurate.loader.ConfigurationLoader" -> {
if (defaultConfig == null) {
holder.registerProblem(
variable.nameIdentifier ?: variable,
- "Injected ConfigurationLoader must be annotated with @DefaultConfig.",
+ "Injected ${classType.name} must be annotated with @DefaultConfig.",
ProblemHighlightType.GENERIC_ERROR,
AddAnnotationFix(SpongeConstants.DEFAULT_CONFIG_ANNOTATION, annotationsOwner),
)
@@ -257,7 +259,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
if (configDir != null) {
holder.registerProblem(
configDir,
- "Injected ConfigurationLoader cannot be annotated with @ConfigDir.",
+ "Injected ${classType.name} cannot be annotated with @ConfigDir.",
ProblemHighlightType.GENERIC_ERROR,
QuickFixFactory.getInstance().createDeleteFix(configDir, "Remove @ConfigDir"),
)
@@ -267,7 +269,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
val ref = classType.reference
holder.registerProblem(
ref,
- "Injected ConfigurationLoader must have a generic parameter.",
+ "Injected ${classType.name} must have a generic parameter.",
ProblemHighlightType.GENERIC_ERROR,
MissingConfLoaderTypeParamFix(ref),
)
@@ -275,14 +277,17 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
classType.parameters.firstOrNull()?.let { param ->
val paramType = param as? PsiClassReferenceType ?: return@let
val paramTypeFQName = paramType.fullQualifiedName ?: return@let
- if (paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode") {
+ if (
+ paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode" &&
+ paramTypeFQName != "org.spongepowered.configurate.CommentedConfigurationNode"
+ ) {
val ref = param.reference
holder.registerProblem(
ref,
"Injected ConfigurationLoader generic parameter must be " +
"CommentedConfigurationNode.",
ProblemHighlightType.GENERIC_ERROR,
- WrongConfLoaderTypeParamFix(ref),
+ WrongConfLoaderTypeParamFix(classType.className, ref),
)
}
}
@@ -371,7 +376,8 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
}
}
- class WrongConfLoaderTypeParamFix(ref: PsiJavaCodeReferenceElement) : LocalQuickFixOnPsiElement(ref) {
+ class WrongConfLoaderTypeParamFix(private val clazzName: String, param: PsiJavaCodeReferenceElement) :
+ LocalQuickFixOnPsiElement(param) {
override fun getFamilyName(): String = name
@@ -379,7 +385,11 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText(
- "ninja.leaping.configurate.commented.CommentedConfigurationNode",
+ when (clazzName) {
+ "ninja.leaping.configurate.loader.ConfigurationLoader" ->
+ "ninja.leaping.configurate.commented.CommentedConfigurationNode"
+ else -> { "org.spongepowered.configurate.CommentedConfigurationNode" }
+ },
startElement,
)
startElement.replace(newRef)
@@ -393,11 +403,23 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() {
override fun getText(): String = "Insert generic parameter"
override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) {
- val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText(
- "ninja.leaping.configurate.loader.ConfigurationLoader" +
- "",
- startElement,
- )
+ val newRef: PsiElement = if (
+ JavaPsiFacade.getInstance(project)
+ .findPackage("ninja.leaping.configurate") != null
+ ) {
+ JavaPsiFacade.getElementFactory(project).createReferenceFromText(
+ "ninja.leaping.configurate.loader.ConfigurationLoader" +
+ "",
+ startElement
+ )
+ } else {
+ JavaPsiFacade.getElementFactory(project).createReferenceFromText(
+ "org.spongepowered.configurate.loader.ConfigurationLoader" +
+ "",
+ startElement
+ )
+ }
+
startElement.replace(newRef)
}
}
diff --git a/src/main/kotlin/util/MinecraftTemplates.kt b/src/main/kotlin/util/MinecraftTemplates.kt
index 368e30cee..5674d58c4 100644
--- a/src/main/kotlin/util/MinecraftTemplates.kt
+++ b/src/main/kotlin/util/MinecraftTemplates.kt
@@ -87,7 +87,9 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory {
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_19_MAIN_CLASS_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_19_3_MAIN_CLASS_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_20_MAIN_CLASS_TEMPLATE))
+ forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_20_6_MAIN_CLASS_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_20_CONFIG_TEMPLATE))
+ forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_21_CONFIG_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_BUILD_GRADLE_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_GRADLE_PROPERTIES_TEMPLATE))
forgeGroup.addTemplate(FileTemplateDescriptor(FG3_SETTINGS_GRADLE_TEMPLATE))
@@ -214,7 +216,9 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory {
const val FG3_1_19_MAIN_CLASS_TEMPLATE = "Forge (1.19+) Main Class.java"
const val FG3_1_19_3_MAIN_CLASS_TEMPLATE = "Forge (1.19.3+) Main Class.java"
const val FG3_1_20_MAIN_CLASS_TEMPLATE = "Forge (1.20+) Main Class.java"
+ const val FG3_1_20_6_MAIN_CLASS_TEMPLATE = "Forge (1.20.6+) Main Class.java"
const val FG3_1_20_CONFIG_TEMPLATE = "Forge (1.20+) Config.java"
+ const val FG3_1_21_CONFIG_TEMPLATE = "Forge (1.21+) Config.java"
const val FG3_BUILD_GRADLE_TEMPLATE = "Forge (1.13+) build.gradle"
const val FG3_GRADLE_PROPERTIES_TEMPLATE = "Forge (1.13+) gradle.properties"
const val FG3_SETTINGS_GRADLE_TEMPLATE = "Forge (1.13+) settings.gradle"
@@ -275,8 +279,15 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory {
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_NEOFORGE_MODS_TOML_TEMPLATE = "NeoForge neoforge.mods.toml"
const val NEOFORGE_PACK_MCMETA_TEMPLATE = "NeoForge pack.mcmeta"
+ const val NEOFORGE_1_20_5_MAIN_CLASS_TEMPLATE = "NeoForge (1.20.5) Main Class.java"
+ const val NEOFORGE_1_20_5_CONFIG_TEMPLATE = "NeoForge (1.20.5) Config.java"
+ const val NEOFORGE_1_20_5_BUILD_GRADLE_TEMPLATE = "NeoForge (1.20.5) build.gradle"
+
+ const val NEOFORGE_1_21_CONFIG_TEMPLATE = "NeoForge (1.21) Config.java"
+
const val NEOFORGE_BLOCK_TEMPLATE = "NeoForgeBlock.java"
const val NEOFORGE_ITEM_TEMPLATE = "NeoForgeItem.java"
const val NEOFORGE_PACKET_TEMPLATE = "NeoForgePacket.java"
diff --git a/src/main/kotlin/util/MinecraftVersions.kt b/src/main/kotlin/util/MinecraftVersions.kt
index 8635b79e8..9d19a575c 100644
--- a/src/main/kotlin/util/MinecraftVersions.kt
+++ b/src/main/kotlin/util/MinecraftVersions.kt
@@ -38,10 +38,14 @@ object MinecraftVersions {
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)
+ val MC1_20_5 = SemanticVersion.release(1, 20, 5)
+ val MC1_20_6 = SemanticVersion.release(1, 20, 6)
+ val MC1_21 = SemanticVersion.release(1, 21)
fun requiredJavaVersion(minecraftVersion: SemanticVersion) = when {
minecraftVersion <= MC1_16_5 -> JavaSdkVersion.JDK_1_8
minecraftVersion <= MC1_17_1 -> JavaSdkVersion.JDK_16
- else -> JavaSdkVersion.JDK_17
+ minecraftVersion <= MC1_20_4 -> JavaSdkVersion.JDK_17
+ else -> JavaSdkVersion.JDK_21
}
}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 3078388d1..307909a6e 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -142,6 +142,7 @@
+
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 be6e106e7..e49c87cce 100644
--- a/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft
+++ b/src/main/resources/fileTemplates/j2ee/fabric/fabric_build.gradle.ft
@@ -79,7 +79,8 @@ jar {
// configure the maven publication
publishing {
publications {
- mavenJava(MavenPublication) {
+ create("mavenJava", MavenPublication) {
+ artifactId = project.archives_base_name
from components.java
}
}
diff --git a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20.6+) Main Class.java.ft b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20.6+) Main Class.java.ft
new file mode 100644
index 000000000..11a627ec8
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20.6+) Main Class.java.ft
@@ -0,0 +1,125 @@
+package ${PACKAGE_NAME};
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.client.Minecraft;
+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.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.BuildCreativeModeTabContentsEvent;
+import net.minecraftforge.event.server.ServerStartingEvent;
+import net.minecraftforge.eventbus.api.IEventBus;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.ModLoadingContext;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.config.ModConfig;
+import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
+import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
+import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
+import net.minecraftforge.registries.DeferredRegister;
+import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraftforge.registries.RegistryObject;
+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 = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
+ // Create a Deferred Register to hold Items which will all be registered under the "${MOD_ID}" namespace
+ public static final DeferredRegister- ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID);
+ // Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "examplemod" 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 RegistryObject EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new 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 RegistryObject
- EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties()));
+
+ // Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2
+ public static final RegistryObject
- EXAMPLE_ITEM = ITEMS.register("example_item", () -> new Item(new Item.Properties().food(new FoodProperties.Builder()
+ .alwaysEdible().nutrition(1).saturationModifier(2f).build())));
+
+ // Creates a creative tab with the id "examplemod:example_tab" for the example item, that is placed after the combat tab
+ public static final RegistryObject 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());
+
+ public ${CLASS_NAME}() {
+ IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
+
+ // 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
+ MinecraftForge.EVENT_BUS.register(this);
+
+ // Register the item to a creative tab
+ modEventBus.addListener(this::addCreative);
+
+ // Register our mod's ForgeConfigSpec so that Forge 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");
+ LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT));
+
+ if (Config.logDirtBlock)
+ LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.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/forge/Forge (1.20.6+) Main Class.java.html b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20.6+) Main Class.java.html
new file mode 100644
index 000000000..a4d2b9279
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20.6+) Main Class.java.html
@@ -0,0 +1,25 @@
+
+
+
+
+
This is a built-in file template used to create a new main class for Forge projects 1.20.6 and above
+
+
diff --git a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.ft b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.ft
new file mode 100644
index 000000000..3a49f15d7
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.ft
@@ -0,0 +1,64 @@
+package ${PACKAGE_NAME};
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.event.config.ModConfigEvent;
+import net.minecraftforge.registries.ForgeRegistries;
+
+import java.util.Collections;
+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 ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder();
+
+ private static final ForgeConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER
+ .comment("Whether to log the dirt block on common setup")
+ .define("logDirtBlock", true);
+
+ private static final ForgeConfigSpec.IntValue MAGIC_NUMBER = BUILDER
+ .comment("A magic number")
+ .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE);
+
+ public static final ForgeConfigSpec.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 ForgeConfigSpec.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 ForgeConfigSpec 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 final String itemName && ForgeRegistries.ITEMS.containsKey(ResourceLocation.parse(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 -> ForgeRegistries.ITEMS.getValue(ResourceLocation.parse(itemName)))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.html b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.html
new file mode 100644
index 000000000..7bc7c827e
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.21+) Config.java.html
@@ -0,0 +1,25 @@
+
+
+
+
+
This is a built-in file template used to create a new config class for Forge projects 1.21 and above
+
+
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Config.java.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Config.java.ft
new file mode 100644
index 000000000..cf0c56d8d
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) 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.EventBusSubscriber;
+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 Neo's config APIs
+@EventBusSubscriber(modid = ${CLASS_NAME}.MODID, bus = 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 (1.20.5) Config.java.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Config.java.html
new file mode 100644
index 000000000..f5870882d
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Config.java.html
@@ -0,0 +1,25 @@
+
+
+
+
+
This is a built-in file template used to create a new config class for NeoForge 1.20.5 projects.
+
+
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Main Class.java.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Main Class.java.ft
new file mode 100644
index 000000000..a3a0c2795
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Main Class.java.ft
@@ -0,0 +1,134 @@
+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.network.chat.Component;
+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.ModContainer;
+import net.neoforged.fml.common.EventBusSubscriber;
+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/neoforge.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()
+ .alwaysEdible().nutrition(1).saturationModifier(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()
+ .title(Component.translatable("itemGroup.${MOD_ID}"))
+ .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, ModContainer modContainer)
+ {
+ // 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.
+ // Note that this is necessary if and only if we want *this* class (ExampleMod) to respond directly to events.
+ // Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below.
+ 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
+ modContainer.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
+ @EventBusSubscriber(modid = MODID, bus = 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 (1.20.5) Main Class.java.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Main Class.java.html
new file mode 100644
index 000000000..701f5287f
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) Main Class.java.html
@@ -0,0 +1,25 @@
+
+
+
+
+
This is a built-in file template used to create a new main class for NeoForge 1.20.5 projects.
+
+
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.ft
new file mode 100644
index 000000000..4edce50d1
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.ft
@@ -0,0 +1,151 @@
+plugins {
+ id 'java-library'
+ id 'eclipse'
+ id 'idea'
+ id 'maven-publish'
+ id 'net.neoforged.gradle.userdev' version '${NEOGRADLE_VERSION}'
+}
+
+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/neoforge.mods.toml']) {
+ expand replaceProperties
+ }
+}
+
+// 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"
+ }
+ }
+}
+
+// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior.
+idea {
+ module {
+ downloadSources = true
+ downloadJavadoc = true
+ }
+}
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.html
new file mode 100644
index 000000000..8b175740d
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.20.5) build.gradle.html
@@ -0,0 +1,25 @@
+
+
+
+
+This is a built-in file template used to create a new build.gradle for NeoForge (1.20.5) projects.
+
+
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) Config.java.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) Config.java.ft
new file mode 100644
index 000000000..b04b88c8f
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) 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.EventBusSubscriber;
+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 Neo's config APIs
+@EventBusSubscriber(modid = ${CLASS_NAME}.MODID, bus = 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(ResourceLocation.parse(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(ResourceLocation.parse(itemName)))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) Config.java.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) Config.java.html
new file mode 100644
index 000000000..8ff2f24fa
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge (1.21) Config.java.html
@@ -0,0 +1,25 @@
+
+
+
+
+
This is a built-in file template used to create a new config class for NeoForge 1.21 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
index 15b3f4489..28c32e3b3 100644
--- a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.ft
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge Main Class.java.ft
@@ -4,6 +4,7 @@ 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.network.chat.Component;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
@@ -56,6 +57,7 @@ public class ${CLASS_NAME}
// 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()
+ .title(Component.translatable("itemGroup.${MOD_ID}"))
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
.displayItems((parameters, output) -> {
@@ -76,7 +78,9 @@ public class ${CLASS_NAME}
// 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
+ // Register ourselves for server and other game events we are interested in.
+ // Note that this is necessary if and only if we want *this* class (ExampleMod) to respond directly to events.
+ // Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below.
NeoForge.EVENT_BUS.register(this);
// Register the item to a creative tab
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft
index 9f6eb7544..6de0fe136 100644
--- a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge build.gradle.ft
@@ -3,7 +3,7 @@ plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
- id 'net.neoforged.gradle.userdev' version '7.0.74'
+ id 'net.neoforged.gradle.userdev' version '${NEOGRADLE_VERSION}'
}
version = mod_version
@@ -109,16 +109,22 @@ dependencies {
// 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,
+ 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]
+ expand replaceProperties
}
}
@@ -139,3 +145,11 @@ publishing {
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
}
+
+// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior.
+idea {
+ module {
+ downloadSources = true
+ downloadJavadoc = true
+ }
+}
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft
index 91afe9ac6..3fff05605 100644
--- a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge gradle.properties.ft
@@ -16,7 +16,7 @@ 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,)
+loader_version_range=[${LOADER_VERSION},)
#if (${PARCHMENT_VERSION})
neogradle.subsystems.parchment.minecraftVersion=${PARCHMENT_MC_VERSION}
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft
index e5439855c..7115e3298 100644
--- a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge mods.toml.ft
@@ -46,13 +46,26 @@ authors="${mod_authors}" #optional
# 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)
+# The description text for the mod (multi line!) (#mandatory)
+description='''${mod_description}'''
+
+#set ( $h = "#" )
+# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
#if (${MIXIN_CONFIG})
[[mixins]]
config="${MIXIN_CONFIG}"
+#else
+${h}[[mixins]]
+#config="${mod_id}.mixins.json"
#end
-# The description text for the mod (multi line!) (#mandatory)
-description='''${mod_description}'''
+# The [[accessTransformers]] block allows you to declare where your AT file is.
+# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
+${h}[[accessTransformers]]
+#file="META-INF/accesstransformer.cfg"
+
+# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
+
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies."${mod_id}"]] #optional
# the modid of the dependency
diff --git a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge neoforge.mods.toml.ft b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge neoforge.mods.toml.ft
new file mode 100644
index 000000000..7e8266261
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge neoforge.mods.toml.ft
@@ -0,0 +1,100 @@
+# This is an example neoforge.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)
+
+# The description text for the mod (multi line!) (#mandatory)
+description='''${mod_description}'''
+
+#set ( $h = "#" )
+# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
+#if (${MIXIN_CONFIG})
+[[mixins]]
+config="${MIXIN_CONFIG}"
+#else
+${h}[[mixins]]
+#config="${mod_id}.mixins.json"
+#end
+
+# The [[accessTransformers]] block allows you to declare where your AT file is.
+# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
+${h}[[accessTransformers]]
+#file="META-INF/accesstransformer.cfg"
+
+# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
+
+# 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 neoforge.mods.toml.html b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge neoforge.mods.toml.html
new file mode 100644
index 000000000..eead33ce5
--- /dev/null
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/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 1.20.5 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
index b359a59d3..ada876e2e 100644
--- a/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.ft
+++ b/src/main/resources/fileTemplates/j2ee/neoforge/NeoForge settings.gradle.ft
@@ -7,5 +7,5 @@ pluginManagement {
}
plugins {
- id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
diff --git a/src/test/kotlin/framework/ProjectBuilder.kt b/src/test/kotlin/framework/ProjectBuilder.kt
index 45e4dd2ed..07fdc5edf 100644
--- a/src/test/kotlin/framework/ProjectBuilder.kt
+++ b/src/test/kotlin/framework/ProjectBuilder.kt
@@ -63,6 +63,12 @@ class ProjectBuilder(private val fixture: JavaCodeInsightTestFixture, private va
configure: Boolean = true,
allowAst: Boolean = false,
) = file(path, code, ".nbtt", configure, allowAst)
+ fun json(
+ path: String,
+ @Language("JSON") code: String,
+ configure: Boolean = true,
+ allowAst: Boolean = false,
+ ) = file(path, code, ".json", configure, allowAst)
inline fun dir(path: String, block: ProjectBuilder.() -> Unit) {
val oldIntermediatePath = intermediatePath
diff --git a/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt
new file mode 100644
index 000000000..619fc4ad8
--- /dev/null
+++ b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt
@@ -0,0 +1,264 @@
+/*
+ * 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
+
+import com.demonwav.mcdev.framework.BaseMinecraftTest
+import com.demonwav.mcdev.framework.EdtInterceptor
+import com.demonwav.mcdev.framework.ProjectBuilder
+import com.demonwav.mcdev.framework.createLibrary
+import com.demonwav.mcdev.platform.PlatformType
+import com.demonwav.mcdev.platform.fabric.inspection.FabricEntrypointsInspection
+import com.demonwav.mcdev.util.runWriteTask
+import com.intellij.openapi.roots.ModuleRootModificationUtil
+import com.intellij.openapi.roots.libraries.Library
+import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar
+import org.intellij.lang.annotations.Language
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(EdtInterceptor::class)
+@DisplayName("Fabric Entrypoints Inspection Tests")
+class FabricEntrypointsInspectionTest : BaseMinecraftTest(PlatformType.FABRIC) {
+
+ private var library: Library? = null
+
+ @BeforeEach
+ fun initFabric() {
+ runWriteTask {
+ library = createLibrary(project, "fabric-loader")
+ }
+
+ ModuleRootModificationUtil.updateModel(module) { model ->
+ model.addLibraryEntry(library ?: throw IllegalStateException("Library not created"))
+ }
+ }
+
+ @AfterEach
+ fun cleanupFabric() {
+ library?.let { l ->
+ ModuleRootModificationUtil.updateModel(module) { model ->
+ model.removeOrderEntry(
+ model.findLibraryOrderEntry(l) ?: throw IllegalStateException("Library not found"),
+ )
+ }
+
+ runWriteTask {
+ val table = LibraryTablesRegistrar.getInstance().getLibraryTable(project)
+ table.modifiableModel.let { model ->
+ model.removeLibrary(l)
+ model.commit()
+ }
+ }
+ }
+ }
+
+ private fun doTest(@Language("JSON") json: String, builder: (ProjectBuilder.() -> Unit) = {}) {
+ buildProject {
+ java(
+ "GoodSimpleModInitializer.java",
+ """
+ import net.fabricmc.api.ModInitializer;
+
+ public class GoodSimpleModInitializer implements ModInitializer {
+ @Override
+ public void onInitialize() {
+ }
+
+ public void handle() {}
+ }
+ """.trimIndent()
+ )
+ java(
+ "GoodSimpleClientModInitializer.java",
+ """
+ import net.fabricmc.api.ClientModInitializer;
+
+ public class GoodSimpleClientModInitializer implements ClientModInitializer {
+ @Override
+ public void onInitializeClient() {
+ }
+ }
+ """.trimIndent()
+ )
+ java(
+ "BadSimpleModInitializer.java",
+ """
+ public class BadSimpleModInitializer {
+ public void handle(String param) {}
+ }
+ """.trimIndent()
+ )
+ java(
+ "BadSimpleClientModInitializer.java",
+ """
+ public class BadSimpleClientModInitializer {}
+ """.trimIndent()
+ )
+
+ builder()
+
+ json("fabric.mod.json", json)
+ }
+
+ fixture.enableInspections(FabricEntrypointsInspection::class)
+ fixture.checkHighlighting(false, false, false)
+ }
+
+ @Test
+ fun validInitializers() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ {
+ "value": "GoodSimpleModInitializer"
+ },
+ "GoodSimpleModInitializer::handle"
+ ],
+ "client": [
+ "GoodSimpleClientModInitializer"
+ ]
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun invalidInitializers() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ "GoodSimpleClientModInitializer",
+ {
+ "value": "BadSimpleModInitializer"
+ }
+ ],
+ "client": [
+ "BadSimpleClientModInitializer",
+ "GoodSimpleModInitializer"
+ ]
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun missingEmptyConstructor() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ "BadCtorSimpleModInitializer"
+ ]
+ }
+ }
+ """.trimIndent()
+ ) {
+ java(
+ "BadCtorSimpleModInitializer.java",
+ """
+ import net.fabricmc.api.ModInitializer;
+
+ public class BadCtorSimpleModInitializer implements ModInitializer {
+ public BadCtorSimpleModInitializer(String param) {}
+ }
+ """.trimIndent()
+ )
+ }
+ }
+
+ @Test
+ fun entrypointMethodWithParameter() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ "BadSimpleModInitializer::handle"
+ ]
+ }
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun entrypointInstanceMethodInClassWithNoEmptyCtor() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ "BadTestInitializer::goodInitialize",
+ "BadTestInitializer::badInitialize"
+ ]
+ }
+ }
+ """.trimIndent()
+ ) {
+ java(
+ "BadTestInitializer.java",
+ """
+ public class BadTestInitializer {
+ public BadTestInitializer(String param) {}
+ public static void goodInitialize() {}
+ public void badInitialize() {}
+ }
+ """.trimIndent()
+ )
+ }
+ }
+
+ @Test
+ fun entrypointFieldInitializers() {
+ doTest(
+ """
+ {
+ "entrypoints": {
+ "main": [
+ "ModInitializerContainer::initializer",
+ "ModInitializerContainer::badTypeInitializer"
+ ]
+ }
+ }
+ """.trimIndent()
+ ) {
+ java(
+ "ModInitializerContainer.java",
+ """
+ public class ModInitializerContainer {
+ public static GoodSimpleModInitializer initializer = new GoodSimpleModInitializer();
+ public static String badTypeInitializer = "No...";
+ }
+ """.trimIndent()
+ )
+ }
+ }
+}