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.1 Nightly Status + + 2024.2 + 2024.2 Nightly Status + 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() + ) + } + } +}