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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + + diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft new file mode 100644 index 000000000..f4c8fcbfa --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.ft @@ -0,0 +1,10 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.level.block.Block; + +public class ${NAME} extends Block { + public ${NAME}(Properties properties) { + super(properties); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html new file mode 100644 index 000000000..e0f3b9c75 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeBlock.java.html @@ -0,0 +1,21 @@ + + +A basic block class for NeoForge. diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft new file mode 100644 index 000000000..c4ae0bd57 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.ft @@ -0,0 +1,12 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentCategory; + +public class ${NAME} extends Enchantment { + public ${NAME}(Rarity rarity, EnchantmentCategory category, EquipmentSlot[] slots) { + super(rarity, category, slots); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html new file mode 100644 index 000000000..9efad589f --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeEnchantment.java.html @@ -0,0 +1,21 @@ + + +NeoForge style enchantment class diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft new file mode 100644 index 000000000..e3ebfd411 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.ft @@ -0,0 +1,10 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.item.Item; + +public class ${NAME} extends Item { + public ${NAME}(Properties properties) { + super(properties); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html new file mode 100644 index 000000000..8a91b26ef --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeItem.java.html @@ -0,0 +1,21 @@ + + +An empty NeoForge item class. diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft new file mode 100644 index 000000000..49bac7d46 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeMobEffect.java.ft @@ -0,0 +1,11 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; + +public class ${NAME} extends MobEffect { + public ${NAME}(MobEffectCategory category, int color) { + super(category, color); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html new file mode 100644 index 000000000..a875fe0ae --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgeModEffect.java.html @@ -0,0 +1,21 @@ + + +NeoForge style mob effect class diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft new file mode 100644 index 000000000..3bbb13ba0 --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.ft @@ -0,0 +1,28 @@ +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end +#parse("File Header.java") + +import net.minecraft.network.FriendlyByteBuf; +import net.neoforged.neoforge.network.NetworkEvent; +import java.util.function.Supplier; + +public class ${NAME} { + + public ${NAME}() { + + } + + public ${NAME}(FriendlyByteBuf buf) { + + } + + public void toBytes(FriendlyByteBuf buf) { + + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html new file mode 100644 index 000000000..4fe41befd --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/skeleton/neoforge/NeoForgePacket.java.html @@ -0,0 +1,21 @@ + + +NeoForge style packet class diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 529453a4b..44ca652db 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -46,6 +46,11 @@ creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: creator.ui.soft_depend.label=Soft Depend: creator.ui.mixins.label=Use Mixins: +creator.ui.parchment.label=Parchment: +creator.ui.parchment.include.label=Include: +creator.ui.parchment.include.old_mc.label=Older Minecraft versions +creator.ui.parchment.include.snapshots.label=Snapshot versions +creator.ui.parchment.no_version.message=No versions of Parchment matching your configuration creator.ui.outdated.message=Is the Minecraft project wizard outdated? \
    Create an issue on the MinecraftDev issue tracker. From c42315723f8f6bbac31c08afc2af28c59056fcc8 Mon Sep 17 00:00:00 2001 From: Gurwi30 <52247449+Gurwi30@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:25:40 +0100 Subject: [PATCH 19/20] Added UTF-8 encoding to Bukkit, BungeeCord & Velocity to the build.gradle (#2211) --- .../resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft | 2 ++ .../fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft | 2 ++ .../fileTemplates/j2ee/velocity/Velocity build.gradle.ft | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft b/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft index cb6cf20e2..2a1b61ec5 100644 --- a/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/bukkit/Bukkit build.gradle.ft @@ -23,6 +23,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } diff --git a/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft b/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft index a38a3aec8..1be8224dd 100644 --- a/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/bungeecord/BungeeCord build.gradle.ft @@ -21,6 +21,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } diff --git a/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft b/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft index af27f6dc1..500fbecd8 100644 --- a/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft +++ b/src/main/resources/fileTemplates/j2ee/velocity/Velocity build.gradle.ft @@ -24,6 +24,8 @@ java { } tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { options.release.set(targetJavaVersion) } From 74bd05164313da69762e9ae02be4efb795bbca1f Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 24 Jan 2024 19:24:40 +0000 Subject: [PATCH 20/20] Bump version to 1.7.0 --- gradle.properties | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cfeaec47f..c8db1c13b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 2023.1.5 ideaVersionName = 2023.1 -coreVersion = 1.6.12 +coreVersion = 1.7.0 downloadIdeaSources = true pluginTomlVersion = 231.8109.91 diff --git a/readme.md b/readme.md index 0184cdaad..9d5805c99 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ Minecraft Development for IntelliJ -Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.6.12-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) +Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.7.0-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) ----------------------