From 2f668c0ba579fed17ee06f521ffe1a3bfd9044e8 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 6 Sep 2024 21:55:40 +0200 Subject: [PATCH] :troll: --- .gitattributes | 5 + build.gradle | 1541 +++-------------- dependencies.gradle | 54 - gradle.properties | 200 +-- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 22 +- gradlew.bat | 22 +- settings.gradle | 25 +- .../neverenoughanimations/IItemLocation.java | 16 +- .../neverenoughanimations/NEA.java | 198 ++- .../neverenoughanimations/NEAConfig.java | 18 +- .../animations/HotbarAnimation.java | 40 +- .../animations/ItemHoverAnimation.java | 33 +- .../animations/ItemMoveAnimation.java | 79 +- .../animations/ItemMovePacket.java | 2 +- .../animations/ItemPickupThrowAnimation.java | 64 +- .../animations/OpeningAnimation.java | 58 +- .../animations/SwapHolder.java | 23 +- .../neverenoughanimations/config/Config.java | 62 + .../config/ModConfigMagic.java | 174 ++ .../core/AbstractMixinPlugin.java | 62 + .../neverenoughanimations/core/LateMixin.java | 31 - .../core/MixinPlugins.java | 22 + .../neverenoughanimations/core/NEACore.java | 42 - .../mixin/AbstractContainerMenuMixin.java | 158 ++ .../mixin/AbstractContainerScreenMixin.java | 104 ++ .../core/mixin/ContainerMixin.java | 153 -- .../core/mixin/CreativeSlotMixin.java | 41 - .../core/mixin/GuiContainerMixin.java | 122 -- .../core/mixin/GuiIngameMixin.java | 19 +- .../core/mixin/InventoryPlayerMixin.java | 26 +- .../core/mixin/MinecraftMixin.java | 38 + .../core/mixin/ScreenMixin.java | 28 + .../core/mixin/SlotMixin.java | 21 +- .../core/mixin/SlotWrapperMixin.java | 42 + .../core/mixin/jei/BookmarkOverlayMixin.java | 39 + .../mixin/jei/IngredientListOverlayMixin.java | 42 +- .../mixin/jei/LeftAreaDispatcherMixin.java | 47 - .../core/mixin/mousetweaks/MainMixin.java | 65 +- .../thermal/ContainerInventoryItemMixin.java | 159 -- .../mixin/trashslot/ClientProxyMixin.java | 4 +- .../util/IStringSerializable.java | 7 + .../util/Interpolation.java | 10 +- .../util/Interpolations.java | 105 +- .../neverenoughanimations/lang/en_us.json | 30 + src/main/resources/mcmod.info | 12 - .../mixin.neverenoughanimations.jei.json | 8 +- .../mixin.neverenoughanimations.json | 13 +- ...xin.neverenoughanimations.mousetweaks.json | 4 +- ...everenoughanimations.thermalexpansion.json | 12 - ...mixin.neverenoughanimations.trashslot.json | 12 - .../templates/META-INF/neoforge.mods.toml | 86 + 53 files changed, 1600 insertions(+), 2602 deletions(-) create mode 100644 .gitattributes delete mode 100644 dependencies.gradle create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/config/Config.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/config/ModConfigMagic.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/AbstractMixinPlugin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/LateMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/MixinPlugins.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/NEACore.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerMenuMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerScreenMixin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ContainerMixin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/CreativeSlotMixin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiContainerMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/MinecraftMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ScreenMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotWrapperMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/BookmarkOverlayMixin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/LeftAreaDispatcherMixin.java delete mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/thermal/ContainerInventoryItemMixin.java create mode 100644 src/main/java/com/cleanroommc/neverenoughanimations/util/IStringSerializable.java create mode 100644 src/main/resources/assets/neverenoughanimations/lang/en_us.json delete mode 100644 src/main/resources/mcmod.info delete mode 100644 src/main/resources/mixin.neverenoughanimations.thermalexpansion.json delete mode 100644 src/main/resources/mixin.neverenoughanimations.trashslot.json create mode 100644 src/main/templates/META-INF/neoforge.mods.toml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f811f6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Disable autocrlf on generated files, they always generate with LF +# Add any extra files or paths here to make git stop saying they +# are changed when only line endings change. +src/generated/**/.cache/cache text eol=lf +src/generated/**/*.json text eol=lf diff --git a/build.gradle b/build.gradle index 09eec4b..666a395 100644 --- a/build.gradle +++ b/build.gradle @@ -1,1195 +1,312 @@ -//version: 1720106721 -/* - * DO NOT CHANGE THIS FILE! - * Also, you may replace this file at any time if there is an update available. - * Please check https://github.com/GregTechCEu/Buildscripts/blob/master/build.gradle for updates. - * You can also run ./gradlew updateBuildScript to update your buildscript. - */ - -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import com.gtnewhorizons.retrofuturagradle.MinecraftExtension -import com.gtnewhorizons.retrofuturagradle.mcp.MCPTasks -import com.gtnewhorizons.retrofuturagradle.minecraft.MinecraftTasks -import com.gtnewhorizons.retrofuturagradle.mcp.ReobfuscatedJar -import com.gtnewhorizons.retrofuturagradle.minecraft.RunMinecraftTask -import com.gtnewhorizons.retrofuturagradle.util.Distribution +import java.nio.file.Files import com.modrinth.minotaur.dependencies.ModDependency import com.modrinth.minotaur.dependencies.VersionDependency -import de.undercouch.gradle.tasks.download.DownloadExtension -import org.apache.commons.io.FileUtils -import org.gradle.api.internal.artifacts.configurations.DefaultUnlockedConfiguration -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.gradle.api.tasks.testing.logging.TestLogEvent -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.jetbrains.gradle.ext.Gradle - -import javax.inject.Inject - -import static org.gradle.internal.logging.text.StyledTextOutput.Style plugins { - id 'java' id 'java-library' - id 'base' - id 'eclipse' id 'maven-publish' - id 'org.jetbrains.gradle.plugin.idea-ext' version '1.1.8' - id 'com.gtnewhorizons.retrofuturagradle' version '1.4.0' + id 'net.neoforged.moddev' version '1.0.17' id 'net.darkhax.curseforgegradle' version '1.1.24' apply false id 'com.modrinth.minotaur' version '2.8.7' apply false - id 'com.diffplug.spotless' version '6.13.0' apply false - id 'com.palantir.git-version' version '3.0.0' apply false - id 'com.github.johnrengelman.shadow' version '8.1.1' apply false - id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false - id 'org.jetbrains.kotlin.kapt' version '1.8.0' apply false - id 'com.google.devtools.ksp' version '1.8.0-1.0.9' apply false - id 'de.undercouch.download' version '5.6.0' apply false } -def out = services.get(StyledTextOutputFactory).create('an-output') - - -// Project properties - -// Required properties: we don't know how to handle these being missing gracefully -checkPropertyExists("modName") -checkPropertyExists("modId") -checkPropertyExists("modGroup") -checkPropertyExists("minecraftVersion") // hard-coding this makes it harder to immediately tell what version a mod is in (even though this only really supports 1.12.2) -checkPropertyExists("apiPackage") -checkPropertyExists("accessTransformersFile") -checkPropertyExists("usesMixins") -checkPropertyExists("mixinsPackage") -checkPropertyExists("coreModClass") -checkPropertyExists("containsMixinsAndOrCoreModOnly") - -// Optional properties: we can assume some default behavior if these are missing -propertyDefaultIfUnset("modVersion", "") -propertyDefaultIfUnset("includeMCVersionJar", false) -propertyDefaultIfUnset("autoUpdateBuildScript", false) -propertyDefaultIfUnset("modArchivesBaseName", project.modId) -propertyDefaultIfUnsetWithEnvVar("developmentEnvironmentUserName", "Developer", "DEV_USERNAME") -propertyDefaultIfUnset("additionalJavaArguments", "") -propertyDefaultIfUnset("enableJava17RunTasks", false) -propertyDefaultIfUnset("generateGradleTokenClass", "") -propertyDefaultIfUnset("gradleTokenModId", "") -propertyDefaultIfUnset("gradleTokenModName", "") -propertyDefaultIfUnset("gradleTokenVersion", "") -propertyDefaultIfUnset("useSrcApiPath", false) -propertyDefaultIfUnset("includeWellKnownRepositories", true) -propertyDefaultIfUnset("includeCommonDevEnvMods", true) -propertyDefaultIfUnset("stripForgeRequirements", false) -propertyDefaultIfUnset("noPublishedSources", false) -propertyDefaultIfUnset("forceEnableMixins", false) -propertyDefaultIfUnset("mixinConfigRefmap", "mixins.${project.modId}.refmap.json") -propertyDefaultIfUnsetWithEnvVar("enableCoreModDebug", false, "CORE_MOD_DEBUG") -propertyDefaultIfUnset("generateMixinConfig", true) -propertyDefaultIfUnset("usesShadowedDependencies", false) -propertyDefaultIfUnset("minimizeShadowedDependencies", true) -propertyDefaultIfUnset("relocateShadowedDependencies", true) -propertyDefaultIfUnset("separateRunDirectories", false) -propertyDefaultIfUnset("versionDisplayFormat", '$MOD_NAME \u2212 $VERSION') -propertyDefaultIfUnsetWithEnvVar("modrinthProjectId", "", "MODRINTH_PROJECT_ID") -propertyDefaultIfUnset("modrinthRelations", "") -propertyDefaultIfUnsetWithEnvVar("curseForgeProjectId", "", "CURSEFORGE_PROJECT_ID") -propertyDefaultIfUnset("curseForgeRelations", "") -propertyDefaultIfUnsetWithEnvVar("releaseType", "release", "RELEASE_TYPE") -propertyDefaultIfUnset("generateDefaultChangelog", false) -propertyDefaultIfUnset("customMavenPublishUrl", "") -propertyDefaultIfUnset("mavenArtifactGroup", getDefaultArtifactGroup()) -propertyDefaultIfUnset("enableModernJavaSyntax", false) -propertyDefaultIfUnset("enableSpotless", false) -propertyDefaultIfUnset("enableJUnit", false) -propertyDefaultIfUnsetWithEnvVar("deploymentDebug", false, "DEPLOYMENT_DEBUG") - - -// Project property assertions - -final String javaSourceDir = 'src/main/java/' -final String scalaSourceDir = 'src/main/scala/' -final String kotlinSourceDir = 'src/main/kotlin/' - -final String modGroupPath = modGroup.toString().replace('.' as char, '/' as char) -final String apiPackagePath = apiPackage.toString().replace('.' as char, '/' as char) - -String targetPackageJava = javaSourceDir + modGroupPath -String targetPackageScala = scalaSourceDir + modGroupPath -String targetPackageKotlin = kotlinSourceDir + modGroupPath - -if (!getFile(targetPackageJava).exists() && !getFile(targetPackageScala).exists() && !getFile(targetPackageKotlin).exists()) { - throw new GradleException("Could not resolve \"modGroup\"! Could not find ${targetPackageJava} or ${targetPackageScala} or ${targetPackageKotlin}") +tasks.named('wrapper', Wrapper).configure { + // Define wrapper values here so as to not have to always do so when updating gradlew.properties. + // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with + // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased + // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. + // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) + distributionType = Wrapper.DistributionType.BIN } -if (apiPackage) { - final String endApiPath = modGroupPath + '/' + apiPackagePath - if (useSrcApiPath.toBoolean()) { - targetPackageJava = 'src/api/java/' + endApiPath - targetPackageScala = 'src/api/scala/' + endApiPath - targetPackageKotlin = 'src/api/kotlin/' + endApiPath - } else { - targetPackageJava = javaSourceDir + endApiPath - targetPackageScala = scalaSourceDir + endApiPath - targetPackageKotlin = kotlinSourceDir + endApiPath - } - if (!getFile(targetPackageJava).exists() && !getFile(targetPackageScala).exists() && !getFile(targetPackageKotlin).exists()) { - throw new GradleException("Could not resolve \"apiPackage\"! Could not find ${targetPackageJava} or ${targetPackageScala} or ${targetPackageKotlin}") - } -} +version = mod_version +group = mod_group_id +def major_mc_version = minecraft_version.substring(0, minecraft_version.lastIndexOf('.')) +def injectTagsDir = 'build/generated/sources/injectTags' -if (accessTransformersFile) { - for (atFile in accessTransformersFile.split(",")) { - String targetFile = 'src/main/resources/' + atFile.trim() - if (!getFile(targetFile).exists()) { - throw new GradleException("Could not resolve \"accessTransformersFile\"! Could not find " + targetFile) - } - tasks.deobfuscateMergedJarToSrg.accessTransformerFiles.from(targetFile) - tasks.srgifyBinpatchedJar.accessTransformerFiles.from(targetFile) - } +repositories { + mavenLocal() } -if (usesMixins.toBoolean()) { - if (mixinsPackage.isEmpty()) { - throw new GradleException("\"usesMixins\" requires \"mixinsPackage\" to be set!") - } - final String mixinPackagePath = mixinsPackage.toString().replaceAll('\\.', '/') - targetPackageJava = javaSourceDir + modGroupPath + '/' + mixinPackagePath - targetPackageScala = scalaSourceDir + modGroupPath + '/' + mixinPackagePath - targetPackageKotlin = kotlinSourceDir + modGroupPath + '/' + mixinPackagePath - if (!getFile(targetPackageJava).exists() && !getFile(targetPackageScala).exists() && !getFile(targetPackageKotlin).exists()) { - throw new GradleException("Could not resolve \"mixinsPackage\"! Could not find ${targetPackageJava} or ${targetPackageScala} or ${targetPackageKotlin}") - } +base { + archivesName = "$mod_id-$major_mc_version" } -if (coreModClass) { - final String coreModPath = coreModClass.toString().replaceAll('\\.', '/') - String targetFileJava = javaSourceDir + modGroupPath + '/' + coreModPath + '.java' - String targetFileScala = scalaSourceDir + modGroupPath + '/' + coreModPath + '.scala' - String targetFileScalaJava = scalaSourceDir + modGroupPath + '/' + coreModPath + '.java' - String targetFileKotlin = kotlinSourceDir + modGroupPath + '/' + coreModPath + '.kt' - if (!getFile(targetFileJava).exists() && !getFile(targetFileScala).exists() && !getFile(targetFileScalaJava).exists() && !getFile(targetFileKotlin).exists()) { - throw new GradleException("Could not resolve \"coreModClass\"! Could not find ${targetFileJava} or ${targetFileScala} or ${targetFileScalaJava} or ${targetFileKotlin}") - } +java { + withSourcesJar() } +// Mojang ships Java 21 to end users starting in 1.20.5, so mods should target Java 21. +java.toolchain.languageVersion = JavaLanguageVersion.of(21) -// Plugin application +neoForge { + // Specify the version of NeoForge to use. + version = project.neo_version -// Scala -if (getFile('src/main/scala').exists()) { - apply plugin: 'scala' -} - -if (getFile('src/main/kotlin').exists()) { - apply plugin: 'org.jetbrains.kotlin.jvm' -} - -// Kotlin -pluginManager.withPlugin('org.jetbrains.kotlin.jvm') { - kotlin { - jvmToolchain(8) + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version } - def disabledKotlinTaskList = [ - "kaptGenerateStubsMcLauncherKotlin", - "kaptGenerateStubsPatchedMcKotlin", - "kaptGenerateStubsInjectedTagsKotlin", - "compileMcLauncherKotlin", - "compilePatchedMcKotlin", - "compileInjectedTagsKotlin", - "kaptMcLauncherKotlin", - "kaptPatchedMcKotlin", - "kaptInjectedTagsKotlin", - "kspMcLauncherKotlin", - "kspPatchedMcKotlin", - "kspInjectedTagsKotlin", - ] - tasks.configureEach { task -> - if (task.name in disabledKotlinTaskList) { - task.enabled = false - } - } -} -// Spotless -//noinspection GroovyAssignabilityCheck -project.extensions.add(com.diffplug.blowdryer.Blowdryer, 'Blowdryer', com.diffplug.blowdryer.Blowdryer) // make Blowdryer available in plugin application -if (enableSpotless.toBoolean()) { - apply plugin: 'com.diffplug.spotless' + // This line is optional. Access Transformers are automatically detected + // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') - // Spotless auto-formatter - // See https://github.com/diffplug/spotless/tree/main/plugin-gradle - // Can be locally toggled via spotless:off/spotless:on comments - spotless { - encoding 'UTF-8' + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() - format 'misc', { - target '.gitignore' - - trimTrailingWhitespace() - indentWithSpaces(4) - endWithNewline() + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } - java { - target 'src/main/java/**/*.java', 'src/test/java/**/*.java' // exclude api as they are not our files - - def orderFile = project.file('spotless.importorder') - if (!orderFile.exists()) { - orderFile = Blowdryer.file('spotless.importorder') - } - def formatFile = project.file('spotless.eclipseformat.xml') - if (!formatFile.exists()) { - formatFile = Blowdryer.file('spotless.eclipseformat.xml') - } - toggleOffOn() - importOrderFile(orderFile) - removeUnusedImports() - endWithNewline() - //noinspection GroovyAssignabilityCheck - eclipse('4.19.0').configFile(formatFile) + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } - kotlin { - target 'src/*/kotlin/**/*.kt' - - toggleOffOn() - ktfmt('0.39') - trimTrailingWhitespace() - indentWithSpaces(4) - endWithNewline() - } - scala { - target 'src/*/scala/**/*.scala' - scalafmt('3.7.1') + // 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 { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id } - } -} - -// Git version checking, also checking for if this is a submodule -if (project.file('.git/HEAD').isFile() || project.file('.git').isFile()) { - apply plugin: 'com.palantir.git-version' -} - -// Shadowing -if (usesShadowedDependencies.toBoolean()) { - apply plugin: 'com.github.johnrengelman.shadow' -} + data { + data() -// Configure Java + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') -java { - toolchain { - if (enableModernJavaSyntax.toBoolean()) { - languageVersion.set(JavaLanguageVersion.of(17)) - } else { - languageVersion.set(JavaLanguageVersion.of(8)) + // 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() } - // Azul covers the most platforms for Java 8+ toolchains, crucially including MacOS arm64 - vendor.set(JvmVendorSpec.AZUL) - } - if (!noPublishedSources.toBoolean()) { - withSourcesJar() - } -} -tasks.withType(JavaCompile).configureEach { - options.encoding = 'UTF-8' + // applies to all the run configs above + 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' - if (enableModernJavaSyntax.toBoolean()) { - if (it.name in ['compileMcLauncherJava', 'compilePatchedMcJava']) { - return + // 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 + logLevel = org.slf4j.event.Level.DEBUG } - - sourceCompatibility = 17 - options.release.set(8) - - javaCompiler.set(javaToolchains.compilerFor { - languageVersion.set(JavaLanguageVersion.of(17)) - vendor.set(JvmVendorSpec.AZUL) - }) - } -} - -tasks.withType(ScalaCompile).configureEach { - options.encoding = 'UTF-8' -} - - -// Allow others using this buildscript to have custom gradle code run -if (getFile('addon.gradle').exists()) { - apply from: 'addon.gradle' -} else if (getFile('addon.gradle.kts').exists()) { - apply from: 'addon.gradle.kts' -} - - -// Configure Minecraft - -// Try to gather mod version from git tags if version is not manually specified -if (!modVersion) { - try { - modVersion = gitVersion() - } catch (Exception ignored) { - out.style(Style.Failure).text( - "Mod version could not be determined! Property 'modVersion' is not set, and either git is not installed or no git tags exist.\n" + - "Either specify a mod version in 'gradle.properties', or create at least one tag in git for this project." - ) - modVersion = 'NO-GIT-TAG-SET' - } -} - -if (includeMCVersionJar.toBoolean()){ - version = "${minecraftVersion}-${modVersion}" -} -else { - version = modVersion -} - -group = modGroup - -base { - archivesName = modArchivesBaseName -} - -minecraft { - mcVersion = minecraftVersion - username = developmentEnvironmentUserName.toString() - useDependencyAccessTransformers = true - - // Automatic token injection with RetroFuturaGradle - if (gradleTokenModId) { - injectedTags.put gradleTokenModId, modId - } - if (gradleTokenModName) { - injectedTags.put gradleTokenModName, modName - } - if (gradleTokenVersion) { - injectedTags.put gradleTokenVersion, modVersion - } - - // JVM arguments - extraRunJvmArguments.add("-ea:${modGroup}") - if (usesMixins.toBoolean()) { - extraRunJvmArguments.addAll([ - '-Dmixin.hotSwap=true', - '-Dmixin.checks.interfaces=true', - '-Dmixin.debug.export=true' - ]) - } - - if (enableCoreModDebug.toBoolean()) { - extraRunJvmArguments.addAll([ - '-Dlegacy.debugClassLoading=true', - '-Dlegacy.debugClassLoadingFiner=true', - '-Dlegacy.debugClassLoadingSave=true' - ]) } - if (additionalJavaArguments.size() != 0) { - extraRunJvmArguments.addAll(additionalJavaArguments.split(';')) - } - - if (enableJava17RunTasks.toBoolean()) { - lwjgl3Version = "3.3.2" - } -} - -if (coreModClass) { - for (runTask in ['runClient', 'runServer']) { - tasks.named(runTask).configure { - extraJvmArgs.add("-Dfml.coreMods.load=${modGroup}.${coreModClass}") + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) } } } -if (generateGradleTokenClass) { - tasks.injectTags.outputClassName.set(generateGradleTokenClass) -} - -tasks.named('processIdeaSettings').configure { - dependsOn('injectTags') -} - - -// Repositories +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } +// Include generated tags +sourceSets.main.java { srcDir injectTagsDir} -// Allow unsafe repos but warn -repositories.configureEach { repo -> - if (repo instanceof UrlArtifactRepository) { - if (repo.getUrl() != null && repo.getUrl().getScheme() == "http" && !repo.allowInsecureProtocol) { - logger.warn("Deprecated: Allowing insecure connections for repo '${repo.name}' - add 'allowInsecureProtocol = true'") - repo.allowInsecureProtocol = true - } - } -} - -// Allow adding custom repositories to the buildscript -if (getFile('repositories.gradle').exists()) { - apply from: 'repositories.gradle' -} else if (getFile('repositories.gradle.kts').exists()) { - apply from: 'repositories.gradle.kts' +// Sets up a dependency configuration called 'localRuntime'. +// This configuration should be used instead of 'runtimeOnly' to declare +// a dependency that will be present for runtime testing but that is +// "optional", meaning it will not be pulled by dependents of this mod. +configurations { + runtimeClasspath.extendsFrom localRuntime } repositories { - if (includeWellKnownRepositories.toBoolean() || includeCommonDevEnvMods.toBoolean()) { - exclusiveContent { - forRepository { - //noinspection ForeignDelegate - maven { - name = 'Curse Maven' - url = 'https://www.cursemaven.com' - // url = 'https://beta.cursemaven.com' - } - } - filter { - includeGroup 'curse.maven' - } - } - exclusiveContent { - forRepository { - //noinspection ForeignDelegate - maven { - name = 'Modrinth' - url = 'https://api.modrinth.com/maven' - } - } - filter { - includeGroup 'maven.modrinth' - } - } - maven { - name 'Cleanroom Maven' - url 'https://maven.cleanroommc.com' - } - maven { - name 'BlameJared Maven' - url 'https://maven.blamejared.com' - } - maven { - name 'GTNH Maven' - url 'https://nexus.gtnewhorizons.com/repository/public/' - } - maven { - name 'GTCEu Maven' - url 'https://maven.gtceu.com' - } - } - if (usesMixins.toBoolean() || forceEnableMixins.toBoolean()) { - // need to add this here even if we did not above - if (!includeWellKnownRepositories.toBoolean()) { + exclusiveContent { + forRepository { + //noinspection ForeignDelegate maven { - name 'Cleanroom Maven' - url 'https://maven.cleanroommc.com' + name = 'Curse Maven' + url = 'https://www.cursemaven.com' } } - } - mavenLocal() // Must be last for caching to work -} - - -// Dependencies - -// Configure dependency configurations -configurations { - embed - implementation.extendsFrom(embed) - - if (usesShadowedDependencies.toBoolean()) { - for (config in [compileClasspath, runtimeClasspath, testCompileClasspath, testRuntimeClasspath]) { - config.extendsFrom(shadowImplementation) - config.extendsFrom(shadowCompile) + filter { + includeGroup 'curse.maven' } } - - create("runtimeOnlyNonPublishable") { - description = "Runtime only dependencies that are not published alongside the jar" - canBeConsumed = false - canBeResolved = false + maven { + // location of the maven that hosts JEI files since January 2023 + name = "Jared's maven" + url = "https://maven.blamejared.com/" } - create("devOnlyNonPublishable") { - description = "Runtime and compiletime dependencies that are not published alongside the jar (compileOnly + runtimeOnlyNonPublishable)" - canBeConsumed = false - canBeResolved = false + maven { + // location of a maven mirror for JEI files, as a fallback + name = "ModMaven" + url = "https://modmaven.dev" } - - compileOnly.extendsFrom(devOnlyNonPublishable) - runtimeOnlyNonPublishable.extendsFrom(devOnlyNonPublishable) - runtimeClasspath.extendsFrom(runtimeOnlyNonPublishable) - testRuntimeClasspath.extendsFrom(runtimeOnlyNonPublishable) } -String mixinProviderSpec = 'zone.rong:mixinbooter:9.1' dependencies { - if (usesMixins.toBoolean()) { - annotationProcessor 'org.ow2.asm:asm-debug-all:5.2' - // should use 24.1.1 but 30.0+ has a vulnerability fix - annotationProcessor 'com.google.guava:guava:30.0-jre' - // should use 2.8.6 but 2.8.9+ has a vulnerability fix - annotationProcessor 'com.google.code.gson:gson:2.8.9' - - mixinProviderSpec = modUtils.enableMixins(mixinProviderSpec, mixinConfigRefmap) - api (mixinProviderSpec) { - transitive = false - } - - annotationProcessor(mixinProviderSpec) { - transitive = false - } - } else if (forceEnableMixins.toBoolean()) { - runtimeOnlyNonPublishable(mixinProviderSpec) - } - - if (enableJUnit.toBoolean()) { - testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - } - - if (enableModernJavaSyntax.toBoolean()) { - annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:1.0.1' - compileOnly('com.github.bsideup.jabel:jabel-javac-plugin:1.0.1') { - transitive = false - } - // workaround for https://github.com/bsideup/jabel/issues/174 - annotationProcessor 'net.java.dev.jna:jna-platform:5.13.0' - // Allow jdk.unsupported classes like sun.misc.Unsafe, workaround for JDK-8206937 and fixes Forge crashes in tests. - patchedMinecraft 'me.eigenraven.java8unsupported:java-8-unsupported-shim:1.0.0' - - // allow Jabel to work in tests - testAnnotationProcessor "com.github.bsideup.jabel:jabel-javac-plugin:1.0.1" - testCompileOnly("com.github.bsideup.jabel:jabel-javac-plugin:1.0.1") { - transitive = false // We only care about the 1 annotation class - } - testCompileOnly "me.eigenraven.java8unsupported:java-8-unsupported-shim:1.0.0" - } - - compileOnlyApi 'org.jetbrains:annotations:24.1.0' - annotationProcessor 'org.jetbrains:annotations:24.1.0' - patchedMinecraft('net.minecraft:launchwrapper:1.17.2') { - transitive = false - } - - if ((usesMixins.toBoolean() || forceEnableMixins.toBoolean()) && stripForgeRequirements.toBoolean()) { - runtimeOnlyNonPublishable 'com.cleanroommc:strip-latest-forge-requirements:1.0' - } - - if (includeCommonDevEnvMods.toBoolean()) { - if (!(modId.equals('jei'))) { - implementation 'mezz.jei:jei_1.12.2:4.16.1.302' - } - if (!(modId.equals('theoneprobe'))) { - //noinspection DependencyNotationArgument - implementation rfg.deobf('curse.maven:top-245211:2667280') // TOP 1.4.28 - } - } -} - -pluginManager.withPlugin('org.jetbrains.kotlin.kapt') { - if (usesMixins.toBoolean()) { - dependencies { - kapt(mixinProviderSpec) - } - } -} - -configurations.configureEach { - resolutionStrategy.dependencySubstitution { - substitute module('org.scala-lang:scala-library:2.11.1') using module('org.scala-lang:scala-library:2.11.5') because('To allow mixing with Java 8 targets') - } -} - -if (getFile('dependencies.gradle').exists()) { - apply from: 'dependencies.gradle' -} else if (getFile('dependencies.gradle.kts').exists()) { - apply from: 'dependencies.gradle.kts' -} - - -// Test configuration - -// Ensure tests have access to minecraft classes -sourceSets { - test { - java { - compileClasspath += patchedMc.output + mcLauncher.output - runtimeClasspath += patchedMc.output + mcLauncher.output - } - } -} - -test { - // ensure tests are run with java8 - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) - }.get() - - testLogging { - events TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.FAILED - exceptionFormat TestExceptionFormat.FULL - showExceptions true - showStackTraces true - showCauses true - showStandardStreams true - } - - if (enableJUnit.toBoolean()) { - useJUnitPlatform() - } -} - - -// Resource processing and jar building - -processResources { - // this will ensure that this task is redone when the versions change. - inputs.property 'version', modVersion - inputs.property 'mcversion', minecraftVersion - // Blowdryer puts these files into the resource directory, so - // exclude them from builds (doesn't hurt to exclude even if not present) - exclude('spotless.importorder') - exclude('spotless.eclipseformat.xml') - - // replace stuff in mcmod.info, nothing else - filesMatching('mcmod.info') { fcd -> - fcd.expand( - 'version': modVersion, - 'mcversion': minecraftVersion, - 'modid': modId, - 'modname': modName - ) - } - - if (accessTransformersFile) { - String[] ats = accessTransformersFile.split(',') - ats.each { at -> - rename "(${at})", 'META-INF/$1' - } - } -} - -// Automatically generate a mixin json file if it does not already exist -tasks.register('generateAssets') { - group = 'GT Buildscript' - description = 'Generates a pack.mcmeta, mcmod.info, or mixins.{modid}.json if needed' - doLast { - // pack.mcmeta - def packMcmetaFile = getFile('src/main/resources/pack.mcmeta') - if (!packMcmetaFile.exists()) { - packMcmetaFile.text = """{ - "pack": { - "pack_format": 3, - "description": "${modName} Resource Pack" - } -} -""" - } - - // mcmod.info - def mcmodInfoFile = getFile('src/main/resources/mcmod.info') - if (!mcmodInfoFile.exists()) { - mcmodInfoFile.text = """[{ - "modid": "\${modid}", - "name": "\${modname}", - "description": "An example mod for Minecraft 1.12.2 with Forge", - "version": "\${version}", - "mcversion": "\${mcversion}", - "logoFile": "", - "url": "", - "authorList": [], - "credits": "", - "dependencies": [] -}] -""" - } - - // mixins.{modid}.json - if (usesMixins.toBoolean() && generateMixinConfig.toBoolean()) { - def mixinConfigFile = getFile("src/main/resources/mixins.${modId}.json") - if (!mixinConfigFile.exists()) { - - mixinConfigFile.text = """{ - "package": "${modGroup}.${mixinsPackage}", - "refmap": "${mixinConfigRefmap}", - "target": "@env(DEFAULT)", - "minVersion": "0.8", - "compatibilityLevel": "JAVA_8", - "mixins": [], - "client": [], - "server": [] -} -""" - } - } - } -} - -tasks.named('processResources').configure { - dependsOn('generateAssets') -} - -jar { - manifest { - attributes(getManifestAttributes()) - } - - // Add all embedded dependencies into the jar - from provider { - configurations.embed.collect { - it.isDirectory() ? it : zipTree(it) - } - } - - if (useSrcApiPath && apiPackage) { - from sourceSets.api.output - dependsOn apiClasses - - include "${modGroupPath}/**" - include "assets/**" - include "mcmod.info" - include "pack.mcmeta" - if (accessTransformersFile) { - include "META-INF/${accessTransformersFile}" - } - } -} - -// Configure default run tasks -if (separateRunDirectories.toBoolean()) { - runClient { - workingDir = file('run/client') - } - - runServer { - workingDir = file('run/server') - } -} - -// Create API library jar -tasks.register('apiJar', Jar) { - archiveClassifier.set 'api' - if (useSrcApiPath) { - from(sourceSets.api.java) { - include "${modGroupPath}/${apiPackagePath}/**" - } - from(sourceSets.api.output) { - include "${modGroupPath}/${apiPackagePath}/**" - } - } else { - from(sourceSets.main.java) { - include "${modGroupPath}/${apiPackagePath}/**" - } - - from(sourceSets.main.output) { - include "${modGroupPath}/${apiPackagePath}/**" - } - } + // Example optional 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-${minecraft_version}-common-api:${jei_version}" + compileOnly "mezz.jei:jei-${minecraft_version}-neoforge-api:${jei_version}" + // We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it + implementation "mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}" + + implementation 'curse.maven:mouse_tweaks-60089:5637846' + implementation 'curse.maven:item_borders-513769:5591010' + implementation 'curse.maven:iceberg-520110:5625109' + implementation 'curse.maven:prism_lib-638111:5625115' + // trashslot works without special compat + + // 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. +var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { + 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 + expand replaceProperties + from "src/main/templates", "src/main/java/${mod_group_id.replace('.', '/')}/Tags.java", "src/main/resources/assets/${mod_id}/lang" + into "build/generated/sources/modMetadata" } +// Include the output of "generateModMetadata" as an input directory for the build +// this works with both building through Gradle and the IDE. +sourceSets.main.resources.srcDir generateModMetadata +// To avoid having to run "generateModMetadata" manually, make it run on every project reload +neoForge.ideSyncTask generateModMetadata -// Configure shadow jar task -if (usesShadowedDependencies.toBoolean()) { - tasks.named('shadowJar', ShadowJar).configure { - manifest { - attributes(getManifestAttributes()) - } - // Only shadow classes that are actually used, if enabled - if (minimizeShadowedDependencies.toBoolean()) { - minimize() +// Example configuration to allow publishing using the maven-publish plugin +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java } - configurations = [ - project.configurations.shadowImplementation, - project.configurations.shadowCompile - ] - archiveClassifier.set('dev') - if (relocateShadowedDependencies.toBoolean()) { - relocationPrefix = modGroup + '.shadow' - enableRelocation = true - } - } - configurations.runtimeElements.outgoing.artifacts.clear() - configurations.apiElements.outgoing.artifacts.clear() - configurations.runtimeElements.outgoing.artifact(tasks.named('shadowJar', ShadowJar)) - configurations.apiElements.outgoing.artifact(tasks.named('shadowJar', ShadowJar)) - tasks.named('jar', Jar) { - enabled = false - finalizedBy(tasks.shadowJar) - } - tasks.named('reobfJar', ReobfuscatedJar) { - inputJar.set(tasks.named('shadowJar', ShadowJar).flatMap({it.archiveFile})) } - AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.components.findByName('java') - javaComponent.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { - skip() - } - for (runTask in ['runClient', 'runServer']) { - tasks.named(runTask).configure { - dependsOn('shadowJar') + repositories { + maven { + url "file://${project.projectDir}/repo" } } } -def getManifestAttributes() { - def attributes = [:] - if (coreModClass) { - attributes['FMLCorePlugin'] = "${modGroup}.${coreModClass}" - } - if (!containsMixinsAndOrCoreModOnly.toBoolean() && (usesMixins.toBoolean() || coreModClass)) { - attributes['FMLCorePluginContainsFMLMod'] = true - } - if (accessTransformersFile) { - attributes['FMLAT'] = accessTransformersFile.toString() - } - - if (usesMixins.toBoolean()) { - attributes['ForceLoadAsMod'] = !containsMixinsAndOrCoreModOnly.toBoolean() - } - return attributes -} - - -// LWJGL3ify setup -if (enableJava17RunTasks.toBoolean()) { - - apply plugin: 'de.undercouch.download' - - ext.java17Toolchain = (JavaToolchainSpec spec) -> { - spec.languageVersion.set(JavaLanguageVersion.of(17)) - spec.vendor.set(JvmVendorSpec.matching("jetbrains")) - } - ext.java21Toolchain = (JavaToolchainSpec spec) -> { - spec.languageVersion.set(JavaLanguageVersion.of(21)) - spec.vendor.set(JvmVendorSpec.matching("jetbrains")) - } - - ext.java17DependenciesCfg = (DefaultUnlockedConfiguration) configurations.create("java17Dependencies") { - extendsFrom(configurations.getByName("runtimeClasspath")) // Ensure consistent transitive dependency resolution - canBeConsumed = false - } - ext.java17PatchDependenciesCfg = (DefaultUnlockedConfiguration) configurations.create("java17PatchDependencies") { - canBeConsumed = false - } - - dependencies { - if (modId != 'lwjgl3ify') { - java17Dependencies("io.github.twilightflower:lwjgl3ify:1.0.0") - } - java17PatchDependencies("io.github.twilightflower:lwjgl3ify:1.0.0:forgePatches") { - transitive = false - } - } - - ext.java17JvmArgs = [ - "-Dfile.encoding=UTF-8", - "-Djava.system.class.loader=com.gtnewhorizons.retrofuturabootstrap.RfbSystemClassLoader", - "-Djava.security.manager=allow", - "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED", - "--add-opens", "java.base/java.net=ALL-UNNAMED", - "--add-opens", "java.base/java.nio=ALL-UNNAMED", - "--add-opens", "java.base/java.io=ALL-UNNAMED", - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", - "--add-opens", "java.base/java.text=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED", - "--add-opens", "java.base/jdk.internal.reflect=ALL-UNNAMED", - "--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", - "--add-opens", "jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED,java.naming", - "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", - "--add-opens", "java.desktop/sun.awt.image=ALL-UNNAMED", - "--add-opens", "java.desktop/com.sun.imageio.plugins.png=ALL-UNNAMED", - "--add-opens", "jdk.dynalink/jdk.dynalink.beans=ALL-UNNAMED", - "--add-opens", "java.sql.rowset/javax.sql.rowset.serial=ALL-UNNAMED" - ] - - ext.hotswapJvmArgs = [ - // DCEVM advanced hot reload - "-XX:+AllowEnhancedClassRedefinition", - "-XX:HotswapAgent=fatjar" - ] - - ext.setupHotswapAgent17 = tasks.register("setupHotswapAgent17", SetupHotswapAgentTask, t -> { - t.setTargetForToolchain(java17Toolchain) - }) - - ext.setupHotswapAgent21 = tasks.register("setupHotswapAgent21", SetupHotswapAgentTask, t -> { - t.setTargetForToolchain(java21Toolchain) - }) - - def runClient17Task = tasks.register("runClient17", RunHotswappableMinecraftTask, Distribution.CLIENT, "runClient") - runClient17Task.configure { - dependsOn(setupHotswapAgent17) - setup(project) - javaLauncher = project.javaToolchains.launcherFor(project.java17Toolchain) - } - - def runServer17Task = tasks.register("runServer17", RunHotswappableMinecraftTask, Distribution.DEDICATED_SERVER, "runServer") - runServer17Task.configure { - dependsOn(setupHotswapAgent17) - setup(project) - javaLauncher = project.javaToolchains.launcherFor(project.java17Toolchain) - } - - def runClient21Task = tasks.register("runClient21", RunHotswappableMinecraftTask, Distribution.CLIENT, "runClient") - runClient21Task.configure { - dependsOn(setupHotswapAgent21) - setup(project) - javaLauncher = project.javaToolchains.launcherFor(project.java21Toolchain) - } - - def runServer21Task = tasks.register("runServer21", RunHotswappableMinecraftTask, Distribution.DEDICATED_SERVER, "runServer") - runServer21Task.configure { - dependsOn(setupHotswapAgent21) - setup(project) - javaLauncher = project.javaToolchains.launcherFor(project.java21Toolchain) - } +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation } -abstract class RunHotswappableMinecraftTask extends RunMinecraftTask { - - // IntelliJ doesn't seem to allow pre-set commandline arguments, so we also support an env variable - private boolean enableHotswap = Boolean.valueOf(System.getenv("HOTSWAP")) - - public final Distribution side - public final String superTask - - @Input - boolean getEnableHotswap() { - return enableHotswap - } - - @Option(option = "hotswap", description = "Enables HotSwapAgent for enhanced class reloading under a debugger") - boolean setEnableHotswap(boolean enable) { - enableHotswap = enable - } - - @Inject - RunHotswappableMinecraftTask(Distribution side, String superTask, org.gradle.api.invocation.Gradle gradle) { - super(side, gradle) - - this.side = side - this.superTask = superTask - setGroup("Modded Minecraft") - setDescription("Runs the modded " + side.name().toLowerCase(Locale.ROOT) + " using modern Java and lwjgl3ify") - this.getLwjglVersion().set(3) - } - - void setup(Project project) { - final MinecraftExtension minecraft = project.getExtensions().getByType(MinecraftExtension.class) - final MCPTasks mcpTasks = project.getExtensions().getByType(MCPTasks.class) - final MinecraftTasks mcTasks = project.getExtensions().getByType(MinecraftTasks.class) - - this.getExtraJvmArgs().addAll((List) project.property("java17JvmArgs")) - if (getEnableHotswap()) { - this.getExtraJvmArgs().addAll((List) project.property("hotswapJvmArgs")) - } - - this.classpath(project.property("java17PatchDependenciesCfg")) - this.classpath(mcpTasks.getTaskPackageMcLauncher()) - this.classpath(mcpTasks.getTaskPackagePatchedMc()) - this.classpath(mcpTasks.getPatchedConfiguration()) - this.classpath(project.getTasks().named("jar")) - this.classpath(project.property("java17DependenciesCfg")) - - super.setup(project) - - dependsOn( - mcpTasks.getLauncherSources().getClassesTaskName(), - mcTasks.getTaskDownloadVanillaAssets(), - mcpTasks.getTaskPackagePatchedMc(), - "jar" - ) - - getMainClass().set((side == Distribution.CLIENT) ? "GradleStart" : "GradleStartServer") - getUsername().set(minecraft.getUsername()) - getUserUUID().set(minecraft.getUserUUID()) - if (side == Distribution.DEDICATED_SERVER) { - getExtraArgs().add("nogui") - } - - systemProperty("gradlestart.bouncerClient", "com.gtnewhorizons.retrofuturabootstrap.Main") - systemProperty("gradlestart.bouncerServer", "com.gtnewhorizons.retrofuturabootstrap.Main") - - if (project.usesMixins.toBoolean()) { - this.extraJvmArgs.addAll(project.provider(() -> { - def mixinCfg = project.configurations.detachedConfiguration(project.dependencies.create(project.mixinProviderSpec)) - mixinCfg.canBeConsumed = false - mixinCfg.canBeResolved = true - mixinCfg.transitive = false - enableHotswap ? ["-javaagent:" + mixinCfg.singleFile.absolutePath] : [] - })) - } +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true } } -abstract class SetupHotswapAgentTask extends DefaultTask { +var injectTagsTask = tasks.register("injectTags") { + group = "brachy" - @OutputFile - abstract RegularFileProperty getTargetFile() + def outDir = projectDir.toString() + '/' + injectTagsDir + def replacements = ['MODID': mod_id, 'NAME': mod_name, 'VERSION': mod_version] + def outClass = "${mod_group_id}.Tags" - void setTargetForToolchain(Action spec) { - getTargetFile().set(project.javaToolchains.launcherFor(spec).map { - it.metadata.installationPath.file("lib/hotswap/hotswap-agent.jar") - }) - } - - @Inject - SetupHotswapAgentTask() { - setGroup("GT Buildscript") - setDescription("Installs a recent version of HotSwapAgent into the Java runtime directory") - onlyIf("Run only if not already installed", t -> !((SetupHotswapAgentTask) t).getTargetFile().getAsFile().get().exists()) - } - - @TaskAction - void installHSA() { - final String url = 'https://github.com/HotswapProjects/HotswapAgent/releases/download/1.4.2-SNAPSHOT/hotswap-agent-1.4.2-SNAPSHOT.jar' - final File target = getTargetFile().getAsFile().get() - final File parent = target.getParentFile() - FileUtils.forceMkdir(parent) - final DownloadExtension download = getProject().getExtensions().findByType(DownloadExtension.class) - download.run(ds -> { + doFirst { + final int lastDot = outClass.lastIndexOf('.'); + final String outPackage = (lastDot >= 0) ? outClass.substring(0, lastDot) : null; + final String outClassName = (lastDot >= 0) ? outClass.substring(lastDot + 1) : outClass; + final String outPath = (outPackage == null ? "" : outPackage.replace('.', '/') + "/") + outClassName + ".java"; + final File outputDir = new File(outDir) + if (outputDir.isDirectory()) { try { - ds.src(url) - } catch (MalformedURLException e) { - throw new RuntimeException(e) + outputDir.deleteDir() + } catch (IOException e) { + getLogger().warn("Could not clean output directory {}", outputDir, e); } - ds.dest(target) - ds.overwrite(false) - ds.tempAndMove(true) - }) - } -} - - -// IDE Configuration - -eclipse { - classpath { - downloadSources = true - downloadJavadoc = true - } -} - -idea { - module { - inheritOutputDirs true - downloadJavadoc true - downloadSources true - } - project { - settings { - runConfigurations { - '1. Setup Workspace'(Gradle) { - taskNames = ['setupDecompWorkspace'] - } - '2. Run Client'(Gradle) { - taskNames = ['runClient'] - } - if (enableJava17RunTasks.toBoolean()) { - '2a. Run Client (Java 17)'(Gradle) { - taskNames = ['runClient17'] - } - '2b. Run Client (Java 21)'(Gradle) { - taskNames = ['runClient21'] - } - } - '3. Run Server'(Gradle) { - taskNames = ['runServer'] - } - if (enableJava17RunTasks.toBoolean()) { - '3a. Run Server (Java 17)'(Gradle) { - taskNames = ['runServer17'] - } - '3b. Run Server (Java 21)'(Gradle) { - taskNames = ['runServer21'] - } - } - '4. Run Obfuscated Client'(Gradle) { - taskNames = ['runObfClient'] - } - '5. Run Obfuscated Server'(Gradle) { - taskNames = ['runObfServer'] - } - if (enableSpotless.toBoolean()) { - '6. Apply Spotless'(Gradle) { - taskNames = ["spotlessApply"] - } - '7. Build Jars'(Gradle) { - taskNames = ['build'] - } - } else { - '6. Build Jars'(Gradle) { - taskNames = ['build'] - } - } - 'Update Buildscript'(Gradle) { - taskNames = ['updateBuildScript'] - } - 'FAQ'(Gradle) { - taskNames = ['faq'] - } - } - compiler.javac { - afterEvaluate { - javacAdditionalOptions = '-encoding utf8' - moduleJavacAdditionalOptions = [ - (project.name + '.main'): tasks.compileJava.options.compilerArgs.collect { - '"' + it + '"' - }.join(' ') - ] - } + } + final File outFile = new File(outputDir, outPath) + outFile.getParentFile().mkdirs() + final StringBuilder outWriter = new StringBuilder(); + if (outPackage != null) { + outWriter.append("package "); + outWriter.append(outPackage); + outWriter.append(";\n\n"); + } + outWriter.append("// Auto-generated tags from Gradle\n"); + outWriter.append("public class "); + outWriter.append(outClassName); + outWriter.append(" {\n private "); + outWriter.append(outClassName); + outWriter.append("() {}\n\n"); + for (Map.Entry entry : replacements.entrySet()) { + final Object e = entry.getValue(); + String eType, eJava; + final String identifier = entry.getKey(); + /*if (!isValidJavaIdentifier(identifier)) { + throw new InvalidUserDataException( + "Tag injection identifier " + identifier + "is not a valid Java identifier!"); + }*/ + if (e instanceof Integer) { + eType = "int" + eJava = Integer.toString((Integer) e) + } else { + eType = "String"; + //eJava = '"' + StringEscapeUtils.escapeJava(e.toString()) + '"' + eJava = '"' + e.toString() + '"' } + outWriter.append(" public static final "); + outWriter.append(eType); + outWriter.append(' '); + outWriter.append(identifier); + outWriter.append(" = "); + outWriter.append(eJava); + outWriter.append(";\n"); } + outWriter.append("}\n"); + println(outFile) + println(outFile.isDirectory()) + outFile.createNewFile() + outFile.text = outWriter.toString() } } +neoForge.ideSyncTask injectTagsTask // Deployment def final modrinthApiKey = providers.environmentVariable('MODRINTH_API_KEY') def final cfApiKey = providers.environmentVariable('CURSEFORGE_API_KEY') final boolean isCIEnv = providers.environmentVariable('CI').getOrElse('false').toBoolean() -if (isCIEnv || deploymentDebug.toBoolean()) { - artifacts { - if (!noPublishedSources.toBoolean()) { - archives sourcesJar - } - if (apiPackage) { - archives apiJar - } - } -} - -// Changelog generation -tasks.register('generateChangelog') { - group = 'GT Buildscript' - description = 'Generate a default changelog of all commits since the last tagged git commit' - onlyIf { - generateDefaultChangelog.toBoolean() - } - doLast { - def lastTag = getLastTag() - - def changelog = runShell(([ - "git", - "log", - "--date=format:%d %b %Y", - "--pretty=%s - **%an** (%ad)", - "${lastTag}..HEAD" - ] + (sourceSets.main.java.srcDirs + sourceSets.main.resources.srcDirs) - .collect { ['--', it] }).flatten()) - - if (changelog) { - changelog = "Changes since ${lastTag}:\n${{("\n" + changelog).replaceAll("\n", "\n* ")}}" - } - def f = getFile('build/changelog.md') - changelog = changelog ?: 'There have been no changes.' - f.write(changelog, 'UTF-8') - - // Set changelog for Modrinth - if (modrinthApiKey.isPresent() || deploymentDebug.toBoolean()) { - modrinth.changelog.set(changelog) - } - } -} - +// TODO if (cfApiKey.isPresent() || deploymentDebug.toBoolean()) { apply plugin: 'net.darkhax.curseforgegradle' //noinspection UnnecessaryQualifiedReference @@ -1299,213 +416,3 @@ def addModrinthDep(String scope, String type, String name) { } project.modrinth.dependencies.add(dep) } - -if (customMavenPublishUrl) { - String publishedVersion = modVersion - - publishing { - publications { - create('maven', MavenPublication) { - //noinspection GroovyAssignabilityCheck - from components.java - - if (apiPackage) { - artifact apiJar - } - - // providers is not available here, use System for getting env vars - groupId = System.getenv('ARTIFACT_GROUP_ID') ?: project.mavenArtifactGroup - artifactId = System.getenv('ARTIFACT_ID') ?: project.modArchivesBaseName - version = System.getenv('RELEASE_VERSION') ?: publishedVersion - } - } - - repositories { - maven { - url = customMavenPublishUrl - allowInsecureProtocol = !customMavenPublishUrl.startsWith('https') - credentials { - username = providers.environmentVariable('MAVEN_USER').getOrElse('NONE') - password = providers.environmentVariable('MAVEN_PASSWORD').getOrElse('NONE') - } - } - } - } -} - -def getSecondaryArtifacts() { - def secondaryArtifacts = [usesShadowedDependencies.toBoolean() ? tasks.shadowJar : tasks.jar] - if (!noPublishedSources.toBoolean()) secondaryArtifacts += [sourcesJar] - if (apiPackage) secondaryArtifacts += [apiJar] - return secondaryArtifacts -} - -def getReleaseType() { - String type = project.releaseType - if (!(type in ['release', 'beta', 'alpha'])) { - throw new Exception("Release type invalid! Found \"" + type + "\", allowed: \"release\", \"beta\", \"alpha\"") - } - return type -} - -/* - * If CHANGELOG_LOCATION env var is set, that takes highest precedence. - * Next, if 'generateDefaultChangelog' option is enabled, use that. - * Otherwise, try to use a CHANGELOG.md file at root directory. - */ -def getChangelog() { - def final changelogEnv = providers.environmentVariable('CHANGELOG_LOCATION') - if (changelogEnv.isPresent()) { - return new File(changelogEnv.get()) - } - if (generateDefaultChangelog.toBoolean()) { - return getFile('build/changelog.md') - } - return getFile('CHANGELOG.md') -} - - -// Buildscript updating - -def buildscriptGradleVersion = '8.5' - -tasks.named('wrapper', Wrapper).configure { - gradleVersion = buildscriptGradleVersion -} - -tasks.register('updateBuildScript') { - group = 'GT Buildscript' - description = 'Updates the build script to the latest version' - - if (gradle.gradleVersion != buildscriptGradleVersion && !Boolean.getBoolean('DISABLE_BUILDSCRIPT_GRADLE_UPDATE')) { - dependsOn('wrapper') - } - - doLast { - if (performBuildScriptUpdate()) return - print('Build script already up to date!') - } -} - -if (!project.getGradle().startParameter.isOffline() && !Boolean.getBoolean('DISABLE_BUILDSCRIPT_UPDATE_CHECK') && isNewBuildScriptVersionAvailable()) { - if (autoUpdateBuildScript.toBoolean()) { - performBuildScriptUpdate() - } else { - out.style(Style.SuccessHeader).println("Build script update available! Run 'gradle updateBuildScript'") - if (gradle.gradleVersion != buildscriptGradleVersion) { - out.style(Style.SuccessHeader).println("updateBuildScript can update gradle from ${gradle.gradleVersion} to ${buildscriptGradleVersion}\n") - } - } -} - -static URL availableBuildScriptUrl() { - new URL("https://raw.githubusercontent.com/GregTechCEu/Buildscripts/master/build.gradle") -} - -static URL availableSettingsGradleUrl() { - new URL("https://raw.githubusercontent.com/GregTechCEu/Buildscripts/master/settings.gradle") -} - -boolean performBuildScriptUpdate() { - if (isNewBuildScriptVersionAvailable()) { - def buildscriptFile = getFile("build.gradle") - def settingsFile = getFile("settings.gradle") - availableBuildScriptUrl().withInputStream { i -> buildscriptFile.withOutputStream { it << i } } - availableSettingsGradleUrl().withInputStream { i -> settingsFile.withOutputStream { it << i } } - def out = services.get(StyledTextOutputFactory).create('buildscript-update-output') - out.style(Style.Success).print("Build script updated. Please REIMPORT the project or RESTART your IDE!") - return true - } - return false -} - -boolean isNewBuildScriptVersionAvailable() { - Map parameters = ["connectTimeout": 10000, "readTimeout": 10000] - - String currentBuildScript = getFile("build.gradle").getText() - String currentBuildScriptHash = getVersionHash(currentBuildScript) - String availableBuildScript = availableBuildScriptUrl().newInputStream(parameters).getText() - String availableBuildScriptHash = getVersionHash(availableBuildScript) - - boolean isUpToDate = currentBuildScriptHash.empty || availableBuildScriptHash.empty || currentBuildScriptHash == availableBuildScriptHash - return !isUpToDate -} - -static String getVersionHash(String buildScriptContent) { - String versionLine = buildScriptContent.find("^//version: [a-z0-9]*") - if (versionLine != null) { - return versionLine.split(": ").last() - } - return "" -} - - -// Faq - -tasks.register('faq') { - group = 'GT Buildscript' - description = 'Prints frequently asked questions about building a project' - doLast { - print("\nTo update this buildscript to the latest version, run 'gradlew updateBuildScript' or run the generated run configuration if you are using IDEA.\n" + - "To set up the project, run the 'setupDecompWorkspace' task, which you can run as './gradlew setupDecompWorkspace' in a terminal, or find in the 'modded minecraft' gradle category.\n\n" + - "To add new dependencies to your project, place them in 'dependencies.gradle', NOT in 'build.gradle' as they would be replaced when the script updates.\n" + - "To add new repositories to your project, place them in 'repositories.gradle'.\n" + - "If you need additional gradle code to run, you can place it in a file named 'addon.gradle' (or either of the above, up to you for organization).\n\n" + - "If your build fails to recognize the syntax of newer Java versions, enable Jabel in your 'gradle.properties' under the option name 'enableModernJavaSyntax'.\n" + - "To see information on how to configure your IDE properly for Java 17, see https://github.com/GregTechCEu/Buildscripts/blob/master/docs/jabel.md\n\n" + - "Report any issues or feature requests you have for this build script to https://github.com/GregTechCEu/Buildscripts/issues\n") - } -} - - -// Helpers - -def getDefaultArtifactGroup() { - def lastIndex = project.modGroup.lastIndexOf('.') - return lastIndex < 0 ? project.modGroup : project.modGroup.substring(0, lastIndex) -} - -def getFile(String relativePath) { - return new File(projectDir, relativePath) -} - -def checkPropertyExists(String propertyName) { - if (!project.hasProperty(propertyName)) { - throw new GradleException("This project requires a property \"" + propertyName + "\"! Please add it your \"gradle.properties\". You can find all properties and their description here: https://github.com/GregTechCEu/Buildscripts/blob/main/gradle.properties") - } -} - -def propertyDefaultIfUnset(String propertyName, defaultValue) { - if (!project.hasProperty(propertyName) || project.property(propertyName) == "") { - project.ext.setProperty(propertyName, defaultValue) - } -} - -def propertyDefaultIfUnsetWithEnvVar(String propertyName, defaultValue, String envVarName) { - def envVar = providers.environmentVariable(envVarName) - if (envVar.isPresent()) { - project.ext.setProperty(propertyName, envVar.get()) - } else { - propertyDefaultIfUnset(propertyName, defaultValue) - } -} - -static runShell(command) { - def process = command.execute() - def outputStream = new StringBuffer() - def errorStream = new StringBuffer() - process.waitForProcessOutput(outputStream, errorStream) - - errorStream.toString().with { - if (it) { - throw new GradleException("Error executing ${command}:\n> ${it}") - } - } - return outputStream.toString().trim() -} - -def getLastTag() { - def githubTag = providers.environmentVariable('GITHUB_TAG') - return runShell('git describe --abbrev=0 --tags ' + - (githubTag.isPresent() ? runShell('git rev-list --tags --skip=1 --max-count=1') : '')) -} diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index 2eb76be..0000000 --- a/dependencies.gradle +++ /dev/null @@ -1,54 +0,0 @@ -//file:noinspection DependencyNotationArgument -// TODO remove when fixed in RFG ^ -/* - * Add your dependencies here. Supported configurations: - * - api("group:name:version:classifier"): if you use the types from this dependency in the public API of this mod - * Available at runtime and compiletime for mods depending on this mod - * - implementation("g:n:v:c"): if you need this for internal implementation details of the mod, but none of it is visible via the public API - * Available at runtime but not compiletime for mods depending on this mod - * - compileOnly("g:n:v:c"): if the mod you're building doesn't need this dependency during runtime at all, e.g. for optional mods - * Not available at all for mods depending on this mod, only visible at compiletime for this mod - * - compileOnlyApi("g:n:v:c"): like compileOnly, but also visible at compiletime for mods depending on this mod - * Available at compiletime but not runtime for mods depending on this mod - * - runtimeOnlyNonPublishable("g:n:v:c"): if you want to include a mod in this mod's runClient/runServer runs, but not publish it as a dependency - * Not available at all for mods depending on this mod, only visible at runtime for this mod - * - devOnlyNonPublishable("g:n:v:c"): a combination of runtimeOnlyNonPublishable and compileOnly for dependencies present at both compiletime and runtime, - * but not published as Maven dependencies - useful for RFG-deobfuscated dependencies or local testing - * - runtimeOnly("g:n:v:c"): if you don't need this at compile time, but want it to be present at runtime - * Available at runtime for mods depending on this mod - * - annotationProcessor("g:n:v:c"): mostly for java compiler plugins, if you know you need this, use it, otherwise don't worry - * - testCONFIG("g:n:v:c") - replace CONFIG by one of the above (except api), same as above but for the test sources instead of main - * - * - shadowImplementation("g:n:v:c"): effectively the same as API, but the dependency is included in your jar under a renamed package name - * Requires you to enable usesShadowedDependencies in gradle.properties - * For more info, see https://github.com/GregTechCEu/Buildscripts/blob/master/docs/shadow.md - * - * You can exclude transitive dependencies (dependencies of the chosen dependency) by appending { transitive = false } if needed, - * but use this sparingly as it can break using your mod as another mod's dependency if you're not careful. - * - * To depend on obfuscated jars you can use `devOnlyNonPublishable(rfg.deobf("dep:spec:1.2.3"))` to fetch an obfuscated jar from maven, - * or `devOnlyNonPublishable(rfg.deobf(project.files("libs/my-mod-jar.jar")))` to use a file. - * - * To add a mod with CurseMaven, replace '("g:n:v:c")' in the above with 'rfg.deobf("curse.maven:project_slug-project_id:file_id")' - * Example: devOnlyNonPublishable(rfg.deobf("curse.maven:top-245211:2667280")) - * - * Gradle names for some of the configuration can be misleading, compileOnlyApi and runtimeOnly both get published as dependencies in Maven, but compileOnly does not. - * The buildscript adds runtimeOnlyNonPublishable to also have a runtime dependency that's not published. - * - * For more details, see https://docs.gradle.org/8.4/userguide/java_library_plugin.html#sec:java_library_configurations_graph - */ -dependencies { - runtimeOnlyNonPublishable rfg.deobf('curse.maven:top-245211:2667280') // TOP 1.4.28 - implementation "mezz:jei:4.25.5", { transitive false } - - implementation rfg.deobf("curse.maven:mouse_tweaks_unofficial-461660:4661407") - implementation rfg.deobf("curse.maven:trashslot-235577:2722385") - - compileOnlyApi "codechicken:codechickenlib:3.2.3.358", { transitive false } - compileOnly rfg.deobf('curse.maven:cofh_core-69162:2920433') - compileOnly rfg.deobf('curse.maven:cofh_world-271384:2920434') - compileOnly rfg.deobf('curse.maven:thermal_expansion-69163:2926431') - compileOnly rfg.deobf('curse.maven:thermal_foundation-222880:2926428') - compileOnly rfg.deobf('curse.maven:redstone_flux-270789:2920436') - -} diff --git a/gradle.properties b/gradle.properties index 5750071..71c2ec0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,109 +1,52 @@ -modName = NeverEnoughAnimations - -# This is a case-sensitive string to identify your mod. Convention is to use lower case. -modId = neverenoughanimations - -modGroup = com.cleanroommc.neverenoughanimations - -# Version of your mod. -# This field can be left empty if you want your mod's version to be determined by the latest git tag instead. -modVersion = 1.0.4 - -# Whether to use the old jar naming structure (modid-mcversion-version) instead of the new version (modid-version) -includeMCVersionJar = false - -# The name of your jar when you produce builds, not including any versioning info -modArchivesBaseName = - -# Will update your build.gradle automatically whenever an update is available -autoUpdateBuildScript = false - -minecraftVersion = 1.12.2 - -# Select a username for testing your mod with breakpoints. You may leave this empty for a random username each time you -# restart Minecraft in development. Choose this dependent on your mod: -# Do you need consistent player progressing (for example Thaumcraft)? -> Select a name -# Do you need to test how your custom blocks interacts with a player that is not the owner? -> leave name empty -# Alternatively this can be set with the 'DEV_USERNAME' environment variable. -developmentEnvironmentUserName = Developer - -# Additional arguments applied to the JVM when launching minecraft -# Syntax: -arg1=value1;-arg2=value2;... -# Example value: -Dmixin.debug.verify=true;-XX:+UnlockExperimentalVMOptions -additionalJavaArguments = - -# Enables using modern java syntax (up to version 17) via Jabel, while still targeting JVM 8. -# See https://github.com/bsideup/jabel for details on how this works. -# Using this requires that you use a Java 17 JDK for development. -enableModernJavaSyntax = true - -# Generate a class with String fields for the mod id, name and version named with the fields below -generateGradleTokenClass = com.cleanroommc.neverenoughanimations.Tags -gradleTokenModId = MODID -gradleTokenModName = MODNAME -gradleTokenVersion = VERSION - -# In case your mod provides an API for other mods to implement you may declare its package here. Otherwise, you can -# leave this property empty. -# Example value: apiPackage = api + modGroup = com.myname.mymodid -> com.myname.mymodid.api -apiPackage = - -# If you want to keep your API code in src/api instead of src/main -useSrcApiPath = false - -# Specify the configuration file for Forge's access transformers here. It must be placed into /src/main/resources/ -# There can be multiple files in a comma-separated list. -# Example value: mymodid_at.cfg,jei_at.cfg -accessTransformersFile = - -# Provides setup for Mixins if enabled. If you don't know what mixins are: Keep it disabled! -usesMixins = true -# Specify the package that contains all of your Mixins. You may only place Mixins in this package or the build will fail! -mixinsPackage = core.mixin -# Automatically generates a mixin config json if enabled, with the name mixins.modid.json -generateMixinConfig = false -# Specify the core mod entry class if you use a core mod. This class must implement IFMLLoadingPlugin! -# Example value: coreModClass = asm.FMLPlugin + modGroup = com.myname.mymodid -> com.myname.mymodid.asm.FMLPlugin -coreModClass = core.NEACore -# If your project is only a consolidation of mixins or a core mod and does NOT contain a 'normal' mod (meaning that -# there is no class annotated with @Mod) you want this to be true. When in doubt: leave it on false! -containsMixinsAndOrCoreModOnly = false - -# Enables Mixins even if this mod doesn't use them, useful if one of the dependencies uses mixins. -forceEnableMixins = false - -# Outputs pre-transformed and post-transformed loaded classes to run/CLASSLOADER_TEMP. Can be used in combination with -# diff to see exactly what your ASM or Mixins are changing in the target file. -# Optionally can be specified with the 'CORE_MOD_DEBUG' env var. Will output a lot of files! -enableCoreModDebug = false - -# Adds CurseMaven, Modrinth Maven, BlameJared maven, and some more well-known 1.12.2 repositories -includeWellKnownRepositories = true - -# Adds JEI and TheOneProbe to your development environment. Adds them as 'implementation', meaning they will -# be available at compiletime and runtime for your mod (in-game and in-code). -# Overrides the above setting to be always true, as these repositories are needed to fetch the mods -includeCommonDevEnvMods = false - -# Some mods require a specific forge version to launch in. When you need to use one of those mods as a dependency, -# and cannot launch with the forge version required, enable this to strip the forge version requirements from that mod. -# This will add 'strip-latest-forge-requirements' as 'runtimeOnlyNonPublishable'. -# Requires useMixins or forceEnableMixins to be true, as the mod uses mixins to function. -stripForgeRequirements = false - - -# If enabled, you may use 'shadowCompile' for dependencies. They will be integrated in your jar. It is your -# responsibility check the licence and request permission for distribution, if required. -usesShadowedDependencies = false -# If disabled, won't remove unused classes from shaded dependencies. Some libraries use reflection to access -# their own classes, making the minimization unreliable. -minimizeShadowedDependencies = true -# If disabled, won't rename the shadowed classes. -relocateShadowedDependencies = true - -# Separate run directories into "run/client" for runClient task, and "run/server" for runServer task. -# Useful for debugging a server and client simultaneously. If not enabled, it will be in the standard location "run/" -separateRunDirectories = false +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +org.gradle.jvmargs=-Xmx1G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +#read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment +# you can also find the latest versions at: https://parchmentmc.org/docs/getting-started +parchment_minecraft_version=1.21 +parchment_mappings_version=2024.07.28 +# 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=1.21.1 +# 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=[1.21.1, 1.22) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.1.35 +# The Neo version range can use any version of Neo as bounds +neo_version_range=[21.1.0,) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) +## 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=neverenoughanimations +# The human-readable display name for the mod. +mod_name=NeverEnoughAnimations +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=All Rights Reserved +# The mod version. See https://semver.org/ +mod_version=1.0.4 +# 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=com.cleanroommc.neverenoughanimations +# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_authors=brachy +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=Example mod description.\nNewline characters can be used and will be replaced properly. + +jei_version=19.16.4.183 +trashslot_version=21.1.1+1.21.1 + +deploymentDebug=false # The display name format of versions published to Curse and Modrinth. $MOD_NAME and $VERSION are available variables. # Default: $MOD_NAME \u2212 $VERSION. \u2212 is the minus character which looks much better than the hyphen minus on Curse. @@ -143,48 +86,3 @@ curseForgeRelations = req:mixin-booter; # Alternatively this can be set with the 'RELEASE_TYPE' environment variable. # Allowed types: release, beta, alpha releaseType = - -# Generate a default changelog for releases. Requires git to be installed, as it uses it to generate a changelog of -# commits since the last tagged release. -generateDefaultChangelog = false - -# Prevent the source code from being published -noPublishedSources = false - - -# Publish to a custom maven location. Follows a few rules: -# Group ID can be set with the 'ARTIFACT_GROUP_ID' environment variable, default to 'project.group' -# Artifact ID can be set with the 'ARTIFACT_ID' environment variable, default to 'project.name' -# Version can be set with the 'RELEASE_VERSION' environment variable, default to 'modVersion' -# For maven credentials: -# Username is set with the 'MAVEN_USER' environment variable, default to "NONE" -# Password is set with the 'MAVEN_PASSWORD' environment variable, default to "NONE" -customMavenPublishUrl = - -# The group for maven artifacts. Defaults to the 'project.modGroup' until the last '.' (if any). -# So 'mymod' becomes 'mymod' and 'com.myname.mymodid' 'becomes com.myname' -mavenArtifactGroup = - -# Enable spotless checks -# Enforces code formatting on your source code -# By default this will use the files found here: https://github.com/GregTechCEu/Buildscripts/tree/master/spotless -# to format your code. However, you can create your own version of these files and place them in your project's -# root directory to apply your own formatting options instead. -enableSpotless = false - -# Enable JUnit testing platform used for testing your code. -# Uses JUnit 5. See guide and documentation here: https://junit.org/junit5/docs/current/user-guide/ -enableJUnit = true - -# Deployment debug setting -# Uncomment this to test deployments to CurseForge and Modrinth -# Alternatively, you can set the 'DEPLOYMENT_DEBUG' environment variable. -deploymentDebug = false - - -# Gradle Settings -# Effectively applies the '--stacktrace' flag by default -org.gradle.logging.stacktrace = all -# Sets default memory used for gradle commands. Can be overridden by user or command line properties. -# This is required to provide enough memory for the Minecraft decompilation process. -org.gradle.jvmargs = -Xmx3G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch literal 43504 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 TZiJi*F`$tkWLn) literal 63375 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≫L+crb^Bz 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 /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle b/settings.gradle index 771def3..ada876e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,30 +1,11 @@ pluginManagement { repositories { - maven { - // RetroFuturaGradle - name 'GTNH Maven' - url 'https://nexus.gtnewhorizons.com/repository/public/' - //noinspection GroovyAssignabilityCheck - mavenContent { - includeGroup 'com.gtnewhorizons' - includeGroup 'com.gtnewhorizons.retrofuturagradle' - } - } - gradlePluginPortal() - mavenCentral() mavenLocal() + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } } } plugins { - id 'com.diffplug.blowdryerSetup' version '1.7.0' - // Automatic toolchain provisioning - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } - -blowdryerSetup { - repoSubfolder 'spotless' - github 'GregTechCEu/Buildscripts', 'tag', 'v1.0.7' -} - -rootProject.name = rootProject.projectDir.getName() diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/IItemLocation.java b/src/main/java/com/cleanroommc/neverenoughanimations/IItemLocation.java index 4616008..3f1f019 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/IItemLocation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/IItemLocation.java @@ -1,9 +1,9 @@ package com.cleanroommc.neverenoughanimations; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; public interface IItemLocation { @@ -24,7 +24,7 @@ static IItemLocation of(Slot slot) { @Override public int nea$getX() { int guiX = 0; - if (Minecraft.getMinecraft().currentScreen instanceof GuiContainer container) { + if (Minecraft.getInstance().screen instanceof AbstractContainerScreen container) { guiX = container.getGuiLeft(); } return NEA.getMouseX() - 8 - guiX; @@ -33,7 +33,7 @@ static IItemLocation of(Slot slot) { @Override public int nea$getY() { int guiY = 0; - if (Minecraft.getMinecraft().currentScreen instanceof GuiContainer container) { + if (Minecraft.getInstance().screen instanceof AbstractContainerScreen container) { guiY = container.getGuiTop(); } return NEA.getMouseY() - 8 - guiY; @@ -46,7 +46,11 @@ static IItemLocation of(Slot slot) { @Override public ItemStack nea$getStack() { - return Minecraft.getMinecraft().player.inventory.getItemStack(); + var screen = Minecraft.getInstance().screen; + if (screen instanceof AbstractContainerScreen containerScreen) { + return containerScreen.getMenu().getCarried(); + } + return ItemStack.EMPTY; } }; diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/NEA.java b/src/main/java/com/cleanroommc/neverenoughanimations/NEA.java index 5f5cc01..d52e5b5 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/NEA.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/NEA.java @@ -1,109 +1,99 @@ package com.cleanroommc.neverenoughanimations; -import com.cleanroommc.neverenoughanimations.animations.ItemHoverAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; -import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; +import com.cleanroommc.neverenoughanimations.animations.*; +import com.cleanroommc.neverenoughanimations.config.ModConfigMagic; +import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraftforge.client.event.GuiOpenEvent; -import net.minecraftforge.client.event.GuiScreenEvent; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.common.config.Config; -import net.minecraftforge.common.config.ConfigManager; -import net.minecraftforge.fml.client.event.ConfigChangedEvent; -import net.minecraftforge.fml.common.Loader; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.Mod.EventHandler; -import net.minecraftforge.fml.common.ModContainer; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.EventPriority; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; -import net.minecraftforge.fml.relauncher.FMLLaunchHandler; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.fml.event.config.ModConfigEvent; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions; +import net.neoforged.neoforge.client.gui.ConfigurationScreen; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import net.neoforged.neoforge.common.NeoForge; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.awt.*; -@Mod(modid = Tags.MODID, - version = Tags.VERSION, - name = Tags.MODNAME, - acceptedMinecraftVersions = "[1.12.2]", - clientSideOnly = true, - dependencies = "required:mixinbooter@[8.8,);") +@Mod(value = NEA.MODID, dist = Dist.CLIENT) public class NEA { - public static final Logger LOGGER = LogManager.getLogger(Tags.MODID); - private static boolean itemBordersLoaded = false, jeiLoaded = false, heiLoaded = false; + public static final String MODID = Tags.MODID; + + public static final Logger LOGGER = LogManager.getLogger(MODID); private static int mouseX, mouseY; - @EventHandler - public void preInit(FMLPreInitializationEvent event) { - MinecraftForge.EVENT_BUS.register(this); - itemBordersLoaded = Loader.isModLoaded("itemborders"); - jeiLoaded = Loader.isModLoaded("jei"); - if (jeiLoaded) { - ModContainer mod = Loader.instance().getIndexedModList().get("jei"); - heiLoaded = "Had Enough Items".equals(mod.getName()); + public NEA(IEventBus eventBus, ModContainer container) { + eventBus.addListener(this::commonSetup); + eventBus.addListener(this::onLoadConfig); + + NeoForge.EVENT_BUS.register(this); + + container.registerConfig(ModConfig.Type.CLIENT, ModConfigMagic.create(NEAConfig.class)); + container.registerExtensionPoint(IConfigScreenFactory.class, (modContainer, screen) -> new ConfigurationScreen(container, screen)); + } + + private void commonSetup(final FMLCommonSetupEvent event) {} + + private void onLoadConfig(final ModConfigEvent event) { + if (event.getConfig().getModId().equals(MODID)) { + ModConfigMagic.load(); } } @SubscribeEvent - public void onGuiTick(TickEvent.ClientTickEvent event) { + public void onGuiTickPre(ClientTickEvent.Pre event) { OpeningAnimation.checkGuiToClose(); - if (event.phase == TickEvent.Phase.END) return; - ItemHoverAnimation.onGuiTick(); + HotbarAnimation.preTick(); } @SubscribeEvent - public void onConfigChanged(ConfigChangedEvent event) { - if (event.getModID().equals(Tags.MODID)) { - NEAConfig.blacklistCache.clear(); - ConfigManager.sync(Tags.MODID, Config.Type.INSTANCE); - } + public void onGuiTickPost(ClientTickEvent.Post event) { + OpeningAnimation.checkGuiToClose(); + ItemHoverAnimation.onGuiTick(); + HotbarAnimation.postTick(); } @SubscribeEvent - public void onGuiOpen(GuiOpenEvent event) { - if (OpeningAnimation.onGuiOpen(event)) return; + public void onGuiOpen(ScreenEvent.Opening event) { ItemHoverAnimation.onGuiOpen(event); ItemMoveAnimation.onGuiOpen(event); ItemPickupThrowAnimation.onGuiOpen(event); } - @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true) - public void onGuiDrawPre(GuiScreenEvent.DrawScreenEvent.Pre event) { - mouseX = event.getMouseX(); - mouseY = event.getMouseY(); - if (NEAConfig.moveAnimationTime > 0 && event.getGui() instanceof GuiContainer) { - GlStateManager.pushMatrix(); - } - } - - @SubscribeEvent(priority = EventPriority.LOWEST) - public void onGuiDrawPost(GuiScreenEvent.DrawScreenEvent.Post event) { - if (NEAConfig.moveAnimationTime > 0 && event.getGui() instanceof GuiContainer container) { - GlStateManager.popMatrix(); - OpeningAnimation.getScale(container); // make sure screens don't get stuck in case they don't render the scale - } + @SubscribeEvent + public void onGuiClose(ScreenEvent.Closing event) { + ItemHoverAnimation.onGuiClose(); + ItemMoveAnimation.onGuiClose(event.getScreen()); + ItemPickupThrowAnimation.onGuiClose(); } - @SubscribeEvent - public void drawDebugInfo(GuiScreenEvent.BackgroundDrawnEvent event) { - if (event.getGui() instanceof GuiContainer container) { - drawScreenDebug(container, event.getMouseX(), event.getMouseY()); - } + @SubscribeEvent(priority = EventPriority.LOW, receiveCanceled = true) + public void onGuiDrawPre(ScreenEvent.Render.Pre event) { + mouseX = event.getMouseX(); + mouseY = event.getMouseY(); + event.getGuiGraphics().pose().pushPose(); } - @SubscribeEvent(priority = EventPriority.LOWEST) - public void onGuiBackgroundDrawn(GuiScreenEvent.BackgroundDrawnEvent event) { - if (NEAConfig.moveAnimationTime > 0 && event.getGui() instanceof GuiContainer container) { - OpeningAnimation.handleScale(container, true); + @SubscribeEvent(priority = EventPriority.HIGH) + public void onGuiDrawPost(ScreenEvent.Render.Post event) { + event.getGuiGraphics().pose().popPose(); + if (event.getScreen() instanceof AbstractContainerScreen container) { + OpeningAnimation.getValue(container); // make sure screens don't get stuck in case they don't render the scale } } @@ -115,53 +105,61 @@ public static int getMouseY() { return mouseY; } - public static void drawScreenDebug(GuiContainer container, int mouseX, int mouseY) { - if (!FMLLaunchHandler.isDeobfuscatedEnvironment() || container.getClass().getName().contains("modularui")) return; - GlStateManager.disableDepth(); - GlStateManager.disableLighting(); - GlStateManager.enableBlend(); + public static void drawScreenDebug(GuiGraphics graphics, AbstractContainerScreen container, int mouseX, int mouseY) { + if (FMLLoader.isProduction() || container.getClass().getName().contains("modularui")) return; + RenderSystem.disableDepthTest(); + RenderSystem.disableBlend(); int screenH = container.height; int color = new java.awt.Color(180, 40, 115).getRGB(); int lineY = screenH - 13; - FontRenderer fr = Minecraft.getMinecraft().fontRenderer; - container.drawString(fr, "Mouse Pos: " + mouseX + ", " + mouseY, 5, lineY, color); + Font fr = Minecraft.getInstance().font; + graphics.drawString(fr, "Mouse Pos: " + mouseX + ", " + mouseY, 5, lineY, color); lineY -= 11; - container.drawString(fr, "Rel. Mouse Pos: " + (mouseX - container.getGuiLeft()) + ", " + (mouseY - container.getGuiTop()), 5, lineY, - color); + graphics.drawString(fr, "Rel. Mouse Pos: " + (mouseX - container.getGuiLeft()) + ", " + (mouseY - container.getGuiTop()), 5, lineY, + color); IItemLocation slot = IItemLocation.of(container.getSlotUnderMouse()); if (slot != null) { lineY -= 11; - container.drawString(fr, "Pos: " + slot.nea$getX() + ", " + slot.nea$getY(), 5, lineY, color); + graphics.drawString(fr, "Pos: " + slot.nea$getX() + ", " + slot.nea$getY(), 5, lineY, color); lineY -= 11; - container.drawString(fr, "Class: " + slot.getClass().getSimpleName(), 5, lineY, color); + graphics.drawString(fr, "Class: " + slot.getClass().getSimpleName(), 5, lineY, color); lineY -= 11; - container.drawString(fr, "Slot Number: " + slot.nea$getSlotNumber(), 5, lineY, color); + graphics.drawString(fr, "Slot Number: " + slot.nea$getSlotNumber(), 5, lineY, color); lineY -= 11; } // dot at mouse pos - Gui.drawRect(mouseX, mouseY, mouseX + 1, mouseY + 1, new Color(10, 230, 10, (int) (0.8 * 155)).getRGB()); + graphics.fill(mouseX, mouseY, mouseX + 1, mouseY + 1, new Color(10, 230, 10, (int) (0.8 * 155)).getRGB()); + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.enableDepthTest(); + } - GlStateManager.color(1f, 1f, 1f, 1f); - GlStateManager.enableLighting(); - GlStateManager.enableDepth(); - GlStateManager.enableRescaleNormal(); - RenderHelper.enableStandardItemLighting(); + public static long time() { + return System.nanoTime() / 1_000_000L; } - public static boolean isItemBordersLoaded() { - return itemBordersLoaded; + public static int withAlpha(int argb, float alpha) { + return withAlpha(argb, (int) (alpha * 255)); } - public static boolean isJeiLoaded() { - return jeiLoaded; + public static int withAlpha(int argb, int alpha) { + argb &= ~(0xFF << 24); + return argb | alpha << 24; } - public static boolean isHeiLoaded() { - return heiLoaded; + public static float getAlphaF(int argb) { + return getAlpha(argb) / 255f; } - public static long time() { - return System.nanoTime() / 1_000_000L; + public static int getAlpha(int argb) { + return argb >> 24 & 255; + } + + public static void drawItem(ItemStack stack, GuiGraphics graphics, Font font, int x, int y) { + var font1 = IClientItemExtensions.of(stack).getFont(stack, IClientItemExtensions.FontContext.ITEM_COUNT); + if (font1 != null) font = font1; + graphics.renderItem(stack, x, y); + graphics.renderItemDecorations(font, stack, x, y); } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/NEAConfig.java b/src/main/java/com/cleanroommc/neverenoughanimations/NEAConfig.java index 3048a03..ff967bd 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/NEAConfig.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/NEAConfig.java @@ -1,15 +1,13 @@ package com.cleanroommc.neverenoughanimations; +import com.cleanroommc.neverenoughanimations.config.Config; import com.cleanroommc.neverenoughanimations.util.Interpolation; import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; -import net.minecraft.client.gui.GuiScreen; -import net.minecraftforge.common.config.Config; +import net.minecraft.client.gui.screens.Screen; -@Config(modid = Tags.MODID) public class NEAConfig { @Config.Name("Hover animation time") - @Config.SlidingOption @Config.RangeInt(min = 0, max = 1000) @Config.Comment("How many millieseconds it takes until an item is scaled to its full size on hover. 0 to disable.") public static int hoverAnimationTime = 100; @@ -21,23 +19,20 @@ public class NEAConfig { public static boolean itemHoverOverlay = false; @Config.Name("Item move animation time") - @Config.SlidingOption @Config.RangeInt(min = 0, max = 1000) @Config.Comment("How many millieseconds it takes until an item has moved to its target (activated on shift click). 0 to disable.") - public static int moveAnimationTime = 100; + public static int moveAnimationTime = 150; @Config.Name("Item move animation easing curve") public static Interpolation moveAnimationCurve = Interpolation.SINE_OUT; @Config.Name("Item (dis)appear animation time") - @Config.SlidingOption @Config.RangeInt(min = 0, max = 1000) @Config.Comment("How many millieseconds it takes until an item has moved to its target (activated on shift click). 0 to disable.") - public static int appearAnimationTime = 100; + public static int appearAnimationTime = 150; @Config.Name("Item (dis)appear animation easing curve") public static Interpolation appearAnimationCurve = Interpolation.SINE_OUT; @Config.Name("Hotbar animation time") - @Config.SlidingOption @Config.RangeInt(min = 0, max = 1000) @Config.Comment("How many millieseconds it takes until the current item marker in the hotbar moved to its new location. 0 to disable.") public static int hotbarAnimationTime = 100; @@ -45,10 +40,9 @@ public class NEAConfig { public static Interpolation hotbarAnimationCurve = Interpolation.QUAD_INOUT; @Config.Name("Opening/Closing animation time") - @Config.SlidingOption @Config.RangeInt(min = 0, max = 1000) @Config.Comment("How many millieseconds it takes until the gui is fully opened. 0 to disable.") - public static int openingAnimationTime = 60; + public static int openingAnimationTime = 90; @Config.Name("Opening/Closing animation easing curve") public static Interpolation openingAnimationCurve = Interpolation.SINE_OUT; @@ -60,7 +54,7 @@ public class NEAConfig { @Config.Ignore public static Object2BooleanOpenHashMap> blacklistCache = new Object2BooleanOpenHashMap<>(); - public static boolean isBlacklisted(GuiScreen screen) { + public static boolean isBlacklisted(Screen screen) { if (screen == null) return true; if (guiAnimationBlacklist.length == 0) return false; if (blacklistCache.containsKey(screen.getClass())) return blacklistCache.getBoolean(screen.getClass()); diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/HotbarAnimation.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/HotbarAnimation.java index 3f293ef..f4180ac 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/HotbarAnimation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/HotbarAnimation.java @@ -3,7 +3,8 @@ import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.NEAConfig; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.entity.player.Player; public class HotbarAnimation { @@ -11,6 +12,25 @@ public class HotbarAnimation { private static int fromX = -1, currentX = -1; private static long startTime = 0; + private static int currentSelected = -1; + + public static int getSelected() { + Player player = Minecraft.getInstance().player; + return player != null ? player.getInventory().selected : -1; + } + + public static void preTick() { + currentSelected = getSelected(); + } + + public static void postTick() { + int selected = getSelected(); + if (currentSelected >= 0 && selected != currentSelected) { + animate(currentSelected, selected); + } + currentSelected = -1; + } + public static void animate(int oldIndex, int newIndex) { if (NEAConfig.hotbarAnimationTime == 0) return; if (isAnimationInProgress()) { @@ -37,26 +57,26 @@ public static void reset() { HotbarAnimation.currentX = -1; } - public static int getX(ScaledResolution sr) { - int def = Minecraft.getMinecraft().player.inventory.currentItem; - if (NEAConfig.hotbarAnimationTime == 0 || oldIndex < 0 || newIndex < 0) return getX(sr, def); + public static int getX(GuiGraphics graphics) { + int def = Minecraft.getInstance().player.getInventory().selected; + if (NEAConfig.hotbarAnimationTime == 0 || oldIndex < 0 || newIndex < 0) return getX(graphics, def); if (def != newIndex) { // index unexpectedly changed, abort animation reset(); - return getX(sr, def); + return getX(graphics, def); } float val = (NEA.time() - HotbarAnimation.startTime) / (float) NEAConfig.hotbarAnimationTime; if (val >= 1f) { // animation ended reset(); - return getX(sr, def); + return getX(graphics, def); } - if (fromX < 0) fromX = getX(sr, oldIndex); - currentX = (int) NEAConfig.hotbarAnimationCurve.interpolate(fromX, getX(sr, newIndex), val); + if (fromX < 0) fromX = getX(graphics, oldIndex); + currentX = (int) NEAConfig.hotbarAnimationCurve.interpolate(fromX, getX(graphics, newIndex), val); return currentX; } - public static int getX(ScaledResolution sr, int index) { - return sr.getScaledWidth() / 2 - 91 - 1 + index * 20; // vanilla behaviour + public static int getX(GuiGraphics graphics, int index) { + return graphics.guiWidth() / 2 - 91 - 1 + index * 20; // vanilla behaviour } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemHoverAnimation.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemHoverAnimation.java index 8a0fef9..c4d6c19 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemHoverAnimation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemHoverAnimation.java @@ -3,39 +3,38 @@ import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.NEAConfig; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.inventory.Slot; -import net.minecraftforge.client.event.GuiOpenEvent; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.neoforged.neoforge.client.event.ScreenEvent; import org.jetbrains.annotations.ApiStatus; -@SideOnly(Side.CLIENT) public class ItemHoverAnimation { - private static GuiContainer lastHoveredGui = null; + private static AbstractContainerScreen lastHoveredGui = null; private static Slot lastHoveredSlot = null; private static final Object2LongOpenHashMap hoveredSlots = new Object2LongOpenHashMap<>(32); @ApiStatus.Internal - public static void onGuiOpen(GuiOpenEvent event) { + public static void onGuiOpen(ScreenEvent.Opening event) { if (NEAConfig.hoverAnimationTime > 0) { - if (!(event.getGui() instanceof GuiContainer)) { - if (lastHoveredGui != null) { - lastHoveredGui = null; - lastHoveredSlot = null; - hoveredSlots.clear(); - } + if (!(event.getNewScreen() instanceof AbstractContainerScreen)) { + onGuiClose(); return; } - if (!NEAConfig.isBlacklisted(event.getGui())) { - lastHoveredGui = (GuiContainer) event.getGui(); + if (!NEAConfig.isBlacklisted(event.getNewScreen())) { + lastHoveredGui = (AbstractContainerScreen) event.getNewScreen(); lastHoveredSlot = null; hoveredSlots.clear(); } } } + public static void onGuiClose() { + lastHoveredGui = null; + lastHoveredSlot = null; + hoveredSlots.clear(); + } + private static void startAnimation(Slot slot, boolean grow) { hoveredSlots.put(slot, NEA.time() * (grow ? 1 : -1)); } @@ -63,7 +62,7 @@ public static void onGuiTick() { } } - public static float getRenderScale(GuiContainer gui, Slot slot) { + public static float getRenderScale(AbstractContainerScreen gui, Slot slot) { if (lastHoveredGui != gui || !isAnimating(slot) || NEAConfig.isBlacklisted(gui)) return 1f; float min = 1f, max = 1.25f; long slotTime = hoveredSlots.getLong(slot); diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMoveAnimation.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMoveAnimation.java index 6c89cd3..fc735d2 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMoveAnimation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMoveAnimation.java @@ -8,16 +8,13 @@ import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import net.minecraftforge.client.event.GuiOpenEvent; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; -import net.minecraftforge.items.ItemHandlerHelper; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.client.event.ScreenEvent; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.ApiStatus; @@ -25,12 +22,11 @@ import java.util.Iterator; import java.util.List; -@SideOnly(Side.CLIENT) public class ItemMoveAnimation { // all itemstack that are currently being animated private static final Int2ObjectOpenHashMap> movingItemsBySource = new Int2ObjectOpenHashMap<>(); - private static GuiContainer lastGui = null; + private static AbstractContainerScreen lastGui = null; // when a stack is moving the stack is actually in the target slot all the time, so we need to trick the player // into thinking there is nothing private static final ObjectArrayList virtualStacks = new ObjectArrayList<>(256); @@ -39,31 +35,36 @@ public class ItemMoveAnimation { private static long lastAnimation = 0; @ApiStatus.Internal - public static void onGuiOpen(GuiOpenEvent event) { + public static void onGuiOpen(ScreenEvent.Opening event) { if (NEAConfig.moveAnimationTime == 0) return; - if (!(event.getGui() instanceof GuiContainer)) { - if (lastGui != null) { - lastGui = null; - movingItemsBySource.clear(); - } + if (!(event.getNewScreen() instanceof AbstractContainerScreen)) { + onGuiClose(lastGui); return; } - if (!NEAConfig.isBlacklisted(event.getGui())) { - lastGui = (GuiContainer) event.getGui(); + if (!NEAConfig.isBlacklisted(event.getNewScreen())) { + lastGui = (AbstractContainerScreen) event.getNewScreen(); movingItemsBySource.clear(); virtualStacks.clear(); virtualStacksUser.clear(); } } - public static ItemStack getVirtualStack(GuiContainer container, Slot slot) { + public static void onGuiClose(Screen screen) { + if (lastGui == screen) { + lastGui = null; + movingItemsBySource.clear(); + } + } + + public static ItemStack getVirtualStack(AbstractContainerScreen container, Slot slot) { return getVirtualStack(container, IItemLocation.of(slot)); } - public static ItemStack getVirtualStack(GuiContainer container, IItemLocation slot) { - return container == lastGui && !NEAConfig.isBlacklisted( - container) && virtualStacks.size() > slot.nea$getSlotNumber() + 1 && virtualStacksUser.getInt( - slot.nea$getSlotNumber() + 1) > 0 ? virtualStacks.get(slot.nea$getSlotNumber() + 1) : null; + public static ItemStack getVirtualStack(AbstractContainerScreen container, IItemLocation slot) { + return container == lastGui && + !NEAConfig.isBlacklisted(container) && + virtualStacks.size() > slot.nea$getSlotNumber() + 1 && + virtualStacksUser.getInt(slot.nea$getSlotNumber() + 1) > 0 ? virtualStacks.get(slot.nea$getSlotNumber() + 1) : null; } @ApiStatus.Internal @@ -107,8 +108,9 @@ public static void queueAnimation(int slot, List packet) { */ @ApiStatus.Internal public static Pair, List> getCandidates(Slot in, List allSlots) { - if (NEAConfig.moveAnimationTime == 0 || NEAConfig.isBlacklisted( - Minecraft.getMinecraft().currentScreen) || NEA.time() - lastAnimation <= 10) { + if (NEAConfig.moveAnimationTime == 0 || + NEAConfig.isBlacklisted(Minecraft.getInstance().screen) || + NEA.time() - lastAnimation <= 10) { return null; } List slots = new ArrayList<>(allSlots.size()); @@ -116,11 +118,11 @@ public static Pair, List> getCandidates(Slot in, List, List slots = candidates.getLeft(); List stacks = candidates.getRight(); // total amount of moved items - int total = oldSource.getCount() - source.getStack().getCount(); + int total = oldSource.getCount() - source.getItem().getCount(); if (total <= 0) return; List packets = new ArrayList<>(); Int2ObjectArrayMap stagedVirtualStacks = new Int2ObjectArrayMap<>(); @@ -157,7 +159,7 @@ public static void handleMove(Slot source, ItemStack oldSource, Pair, total -= newStack.getCount(); stagedVirtualStacks.put(slot.nea$getSlotNumber(), oldStack); } - } else if (ItemHandlerHelper.canItemStacksStack(newStack, oldStack)) { + } else if (ItemStack.isSameItemSameComponents(newStack, oldStack)) { // the stackable check is not really necessary but is still here for safety if (oldStack.getCount() < newStack.getCount()) { // stackable and amount changed -> found @@ -195,14 +197,14 @@ public static void handleMove(Slot source, ItemStack oldSource, Pair, /** * Render all animated item stacks. */ - public static void drawAnimations() { - drawAnimations(Minecraft.getMinecraft().getRenderItem(), Minecraft.getMinecraft().fontRenderer); - } + /*public static void drawAnimations() { + drawAnimations(Minecraft.getInstance().getItemRenderer(), Minecraft.getInstance().font); + }*/ /** * Render all animated item stacks. */ - public static void drawAnimations(RenderItem itemRender, FontRenderer fontRenderer) { + public static void drawAnimations(GuiGraphics graphics, Font font) { for (var iter = movingItemsBySource.values().iterator(); iter.hasNext(); ) { List packets = iter.next(); for (Iterator iterator = packets.iterator(); iterator.hasNext(); ) { @@ -215,11 +217,8 @@ public static void drawAnimations(RenderItem itemRender, FontRenderer fontRender } int x = packet.getDrawX(val); int y = packet.getDrawY(val); - GlStateManager.translate(0, 0, 32f); - FontRenderer font = packet.getMovingStack().getItem().getFontRenderer(packet.getMovingStack()); - if (font == null) font = fontRenderer; - itemRender.renderItemAndEffectIntoGUI(Minecraft.getMinecraft().player, packet.getMovingStack(), x, y); - itemRender.renderItemOverlayIntoGUI(font, packet.getMovingStack(), x, y, null); + graphics.pose().translate(0, 0, 32f); + NEA.drawItem(packet.getMovingStack(), graphics, font, x, y); if (end) { ItemMoveAnimation.updateVirtualStack(packet.getTarget().nea$getSlotNumber(), packet.getTargetStack(), -1); if (packets.size() == 1) { diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMovePacket.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMovePacket.java index fc21ad7..c232a5a 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMovePacket.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemMovePacket.java @@ -3,7 +3,7 @@ import com.cleanroommc.neverenoughanimations.IItemLocation; import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.NEAConfig; -import net.minecraft.item.ItemStack; +import net.minecraft.world.item.ItemStack; public class ItemMovePacket { diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemPickupThrowAnimation.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemPickupThrowAnimation.java index 5989780..9e95bbb 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemPickupThrowAnimation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/ItemPickupThrowAnimation.java @@ -4,43 +4,42 @@ import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.NEAConfig; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.client.event.ScreenEvent; import org.jetbrains.annotations.ApiStatus; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; public class ItemPickupThrowAnimation { private static final Object2LongOpenHashMap animated = new Object2LongOpenHashMap<>(); private static final List removalAnimation = new ArrayList<>(); - private static GuiContainer lastGui; + private static AbstractContainerScreen lastGui; @ApiStatus.Internal - public static void onGuiOpen(GuiOpenEvent event) { + public static void onGuiOpen(ScreenEvent.Opening event) { if (NEAConfig.hoverAnimationTime > 0) { - if (!(event.getGui() instanceof GuiContainer)) { - if (lastGui != null) { - lastGui = null; - animated.clear(); - } + if (!(event.getNewScreen() instanceof AbstractContainerScreen)) { + onGuiClose(); return; } - if (!NEAConfig.isBlacklisted(event.getGui())) { - lastGui = (GuiContainer) event.getGui(); + if (!NEAConfig.isBlacklisted(event.getNewScreen())) { + lastGui = (AbstractContainerScreen) event.getNewScreen(); animated.clear(); } } } + public static void onGuiClose() { + lastGui = null; + animated.clear(); + } + public static void animate(Slot slot) { if (lastGui == null) return; animate(IItemLocation.of(slot)); @@ -61,11 +60,11 @@ public static void animate(int x, int y, ItemStack stack, boolean absolutePos) { animate(new IItemLocation.Impl(x, y, stack)); } - public static float getValue(GuiContainer container, Slot slot) { + public static float getValue(AbstractContainerScreen container, Slot slot) { return getValue(container, IItemLocation.of(slot)); } - public static float getValue(GuiContainer container, IItemLocation slot) { + public static float getValue(AbstractContainerScreen container, IItemLocation slot) { if (lastGui != container || !animated.containsKey(slot)) return 1f; long time = animated.getLong(slot); float val = (NEA.time() - time) / (float) NEAConfig.appearAnimationTime; @@ -76,7 +75,8 @@ public static float getValue(GuiContainer container, IItemLocation slot) { return NEAConfig.appearAnimationCurve.interpolate(0f, 1f, val); } - public static void drawIndependentAnimations(GuiContainer container, RenderItem itemRender, FontRenderer fontRenderer) { + public static void drawIndependentAnimations(AbstractContainerScreen container, GuiGraphics graphics, Font font) { + var pose = graphics.pose(); for (int i = 0, n = removalAnimation.size(); i < n; i++) { IItemLocation slot = removalAnimation.get(i); int x = slot.nea$getX(); @@ -88,28 +88,24 @@ public static void drawIndependentAnimations(GuiContainer container, RenderItem n--; continue; } - GlStateManager.translate(x, y, 0); + pose.translate(x, y, 32f); if (value <= 1f) { - GlStateManager.pushMatrix(); - GlStateManager.translate(8, 8, 0); - GlStateManager.scale(value, value, 1); - GlStateManager.translate(-8, -8, 0); + pose.pushPose(); + pose.translate(8, 8, 0); + pose.scale(value, value, 1); + pose.translate(-8, -8, 0); } else if (!animated.containsKey(slot)) { removalAnimation.remove(i); i--; n--; - GlStateManager.translate(-x, -y, 0); + pose.translate(-x, -y, 0); continue; } - GlStateManager.translate(0, 0, 32f); - FontRenderer font = slot.nea$getStack().getItem().getFontRenderer(slot.nea$getStack()); - if (font == null) font = fontRenderer; - itemRender.renderItemAndEffectIntoGUI(Minecraft.getMinecraft().player, slot.nea$getStack(), 0, 0); - itemRender.renderItemOverlayIntoGUI(font, slot.nea$getStack(), 0, 0, null); + NEA.drawItem(slot.nea$getStack(), graphics, font, 0, 0); if (value <= 1f) { - GlStateManager.popMatrix(); + pose.popPose(); } - GlStateManager.translate(-x, -y, 0); + pose.translate(-x, -y, 0); } } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/OpeningAnimation.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/OpeningAnimation.java index 6630fc7..e681020 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/OpeningAnimation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/OpeningAnimation.java @@ -4,43 +4,44 @@ import com.cleanroommc.neverenoughanimations.NEAConfig; import com.cleanroommc.neverenoughanimations.util.Interpolations; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.jetbrains.annotations.Nullable; public class OpeningAnimation { - public static boolean onGuiOpen(GuiOpenEvent event) { - if (event.getGui() instanceof GuiContainer container) { - if (Minecraft.getMinecraft().currentScreen == null) { - animate(container, true); + public static boolean onGuiOpen(@Nullable Screen newScreen, @Nullable Screen currentScreen, Runnable closeCallback) { + if (newScreen instanceof AbstractContainerScreen container) { + if (currentScreen == null) { + animate(container, true, closeCallback); } - } else if (Minecraft.getMinecraft().currentScreen == lastGui && event.getGui() == null && !shouldCloseLast) { - animate(lastGui, false); - event.setCanceled(true); + } else if (currentScreen == lastGui && newScreen == null && !shouldCloseLast) { + animate(lastGui, false, closeCallback); return true; } return false; } - private static GuiContainer lastGui; - private static GuiContainer animatedGui; + private static AbstractContainerScreen lastGui; + private static AbstractContainerScreen animatedGui; private static long startTime = 0; private static boolean shouldCloseLast = false; - public static void animate(GuiContainer container, boolean open) { + public static void animate(AbstractContainerScreen container, boolean open, Runnable closeCallback) { if (NEAConfig.openingAnimationTime == 0 || NEAConfig.isBlacklisted(container)) return; animatedGui = container; lastGui = container; startTime = NEA.time() * (open ? 1 : -1); + if (!open) closeCallback.run(); } - public static float getScale(GuiContainer container) { + public static float getScale(float value) { float min = 0.75f, max = 1f; - return Interpolations.lerp(min, max, getValue(container)); + return Interpolations.lerp(min, max, value); } - public static float getValue(GuiContainer container) { + public static float getValue(AbstractContainerScreen container) { if (shouldCloseLast) return 0.001f; if (animatedGui != container) return 1f; float val = (NEA.time() - Math.abs(startTime)) / (float) NEAConfig.openingAnimationTime; @@ -49,7 +50,7 @@ public static float getValue(GuiContainer container) { if (val <= 0) { animatedGui = null; shouldCloseLast = true; - return 0f; + return 0.001f; } } else if (val >= 1f) { animatedGui = null; @@ -59,21 +60,24 @@ public static float getValue(GuiContainer container) { return NEAConfig.openingAnimationCurve.interpolate(0f, 1f, val); } - public static boolean handleScale(GuiContainer container, boolean translateToPanel) { - float scale = getScale(container); - if (scale == 1 || NEAConfig.moveAnimationTime == 0) return false; - if (translateToPanel) GlStateManager.translate(container.getGuiLeft(), container.getGuiTop(), 0); - GlStateManager.translate(container.getXSize() / 2f, container.getYSize() / 2f, 0); - GlStateManager.scale(scale, scale, 1f); - GlStateManager.translate(-container.getXSize() / 2f, -container.getYSize() / 2f, 0); - if (translateToPanel) GlStateManager.translate(-container.getGuiLeft(), -container.getGuiTop(), 0); - // GlStateManager.color(1f, 1f, 1f, scale); + public static boolean handleScale(GuiGraphics graphics, AbstractContainerScreen container, boolean translateToPanel) { + float value = getValue(container); + if (value >= 1 || value <= 0 || NEAConfig.openingAnimationTime == 0) return false; + float scale = getScale(value); + if (translateToPanel) graphics.pose().translate(container.getGuiLeft(), container.getGuiTop(), 0); + graphics.pose().translate(container.getXSize() / 2f, container.getYSize() / 2f, 0); + graphics.pose().scale(scale, scale, 1f); + graphics.pose().translate(-container.getXSize() / 2f, -container.getYSize() / 2f, 0); + if (translateToPanel) graphics.pose().translate(-container.getGuiLeft(), -container.getGuiTop(), 0); + // this only applies to text currently + // for textures we would probably need to mixin GuiGraphics and modify the buffer builder to accept color + graphics.setColor(1f, 1f, 1f, value); return true; } public static void checkGuiToClose() { if (shouldCloseLast && lastGui != null) { - Minecraft.getMinecraft().displayGuiScreen(null); + Minecraft.getInstance().setScreen(null); shouldCloseLast = false; lastGui = null; } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/animations/SwapHolder.java b/src/main/java/com/cleanroommc/neverenoughanimations/animations/SwapHolder.java index 8b92e93..55e5203 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/animations/SwapHolder.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/animations/SwapHolder.java @@ -4,12 +4,12 @@ import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.NEAConfig; import net.minecraft.client.Minecraft; -import net.minecraft.entity.player.InventoryPlayer; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import net.minecraftforge.items.SlotItemHandler; -import net.minecraftforge.items.wrapper.PlayerInvWrapper; -import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.items.SlotItemHandler; +import net.neoforged.neoforge.items.wrapper.PlayerInvWrapper; +import net.neoforged.neoforge.items.wrapper.PlayerMainInvWrapper; import java.util.List; @@ -23,15 +23,15 @@ public class SwapHolder { private ItemStack hotbarStack; public boolean init(Slot hoveredSlot, List slots, int hotbarIndex) { - if (NEAConfig.isBlacklisted(Minecraft.getMinecraft().currentScreen)) return false; + if (NEAConfig.isBlacklisted(Minecraft.getInstance().screen)) return false; this.targetSlot = hoveredSlot; this.hotbarSlot = findHotbarSlot(slots, hotbarIndex); if (this.hotbarSlot == null) { reset(); return false; } - this.targetStack = this.targetSlot.getStack(); - this.hotbarStack = this.hotbarSlot.getStack(); + this.targetStack = this.targetSlot.getItem(); + this.hotbarStack = this.hotbarSlot.getItem(); if (this.targetStack.isEmpty() && this.hotbarStack.isEmpty()) { reset(); return false; @@ -98,7 +98,10 @@ public Slot getTargetSlot() { public static Slot findHotbarSlot(List slots, int index) { for (Slot slot : slots) { if (slot.getSlotIndex() != index) continue; - if (slot.inventory instanceof InventoryPlayer || (slot instanceof SlotItemHandler slotItemHandler && (slotItemHandler.getItemHandler() instanceof PlayerMainInvWrapper || slotItemHandler.getItemHandler() instanceof PlayerInvWrapper))) { + if (slot.container instanceof Inventory || + (slot instanceof SlotItemHandler slotItemHandler && + (slotItemHandler.getItemHandler() instanceof PlayerMainInvWrapper || + slotItemHandler.getItemHandler() instanceof PlayerInvWrapper))) { return slot; } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/config/Config.java b/src/main/java/com/cleanroommc/neverenoughanimations/config/Config.java new file mode 100644 index 0000000..e3b6774 --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/config/Config.java @@ -0,0 +1,62 @@ +package com.cleanroommc.neverenoughanimations.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public interface Config { + + /*@Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @interface Builder {}*/ + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Name { + + String value(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Comment { + + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface Ignore {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface RangeInt { + + int min() default Integer.MIN_VALUE; + + int max() default Integer.MAX_VALUE; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface RangeDouble { + + double min() default Double.MIN_VALUE; + + double max() default Double.MAX_VALUE; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.TYPE}) + @interface RequiresMcRestart {} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.TYPE}) + @interface RequiresWorldRestart {} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + @interface Category {} +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/config/ModConfigMagic.java b/src/main/java/com/cleanroommc/neverenoughanimations/config/ModConfigMagic.java new file mode 100644 index 0000000..8eccbac --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/config/ModConfigMagic.java @@ -0,0 +1,174 @@ +package com.cleanroommc.neverenoughanimations.config; + +import com.cleanroommc.neverenoughanimations.NEA; +import com.cleanroommc.neverenoughanimations.Tags; +import com.google.common.base.Joiner; +import it.unimi.dsi.fastutil.Function; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforge.common.ModConfigSpec; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A cursed config helper to parse annotations of fields and turns it into a forge config. + */ +public class ModConfigMagic { + + private static final List configs = new ArrayList<>(); + private static final List translations = new ArrayList<>(); + + private static final ObjectArrayList sectionStack = new ObjectArrayList<>(); + + static { + String title = Tags.NAME + " Config"; + appendTranslation("title", title, false); + appendTranslation("section." + NEA.MODID + ".common.toml.title", title, false); + appendTranslation("section." + NEA.MODID + ".common.toml", title, false); + } + + public static ModConfigSpec create(Class clazz) { + ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); + collectValuesFrom(builder, clazz); + return builder.build(); + } + + private static void collectValuesFrom(ModConfigSpec.Builder builder, Object o) { + boolean isStatic = o instanceof Class; + Class clazz = isStatic ? (Class) o : o.getClass(); + Object instance = isStatic ? null : o; + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Config.Ignore.class) || + !field.canAccess(null) || + Modifier.isStatic(field.getModifiers()) != isStatic) { + continue; + } + Class type = field.getType(); + String path = field.getName(); + Object def; + try { + def = field.get(instance); + if (def == null) { + NEA.LOGGER.warn("Default config value for field {} is null. This is not good!", path); + continue; + } + } catch (IllegalAccessException e) { + continue; + } + if (field.isAnnotationPresent(Config.Category.class)) { + builder.push(path); + sectionStack.push(path); + collectValuesFrom(builder, def); + sectionStack.pop(); + builder.pop(); + continue; + } + if (Modifier.isFinal(field.getModifiers())) continue; + Value value = parseToConfigValue(builder, field, path, type, def); + value.instance = instance; + configs.add(value); + } + } + + private static void appendTranslation(String path, String translation, boolean tooltip) { + if (FMLLoader.isProduction()) return; + StringBuilder b = new StringBuilder().append('\t').append('"').append(NEA.MODID).append('.').append("configuration."); + sectionStack.forEach(s -> b.append(s).append('.')); + b.append(path); + if (tooltip) b.append(".tooltip"); + b.append('"').append(": ").append('"').append(translation).append('"').append(", \n"); + translations.add(b.toString()); + } + + private static Value parseToConfigValue(ModConfigSpec.Builder builder, Field field, String path, Class type, Object def) { + Function converter = o -> o; + ModConfigSpec.ConfigValue value; + boolean hasTooltip = false; + if (field.isAnnotationPresent(Config.Name.class)) { + appendTranslation(path, field.getAnnotation(Config.Name.class).value(), false); + } + if (field.isAnnotationPresent(Config.Comment.class)) { + Config.Comment comment = field.getAnnotation(Config.Comment.class); + builder.comment(comment.value()); + appendTranslation(path, Joiner.on("\\n").join(comment.value()), true); + hasTooltip = true; + } + if (field.isAnnotationPresent(Config.RequiresWorldRestart.class)) { + builder.worldRestart(); + } + if (field.isAnnotationPresent(Config.RequiresMcRestart.class)) { + builder.gameRestart(); + } + if (field.isAnnotationPresent(Config.RangeInt.class) && (type == Integer.class || type == int.class)) { + Config.RangeInt range = field.getAnnotation(Config.RangeInt.class); + value = builder.defineInRange(path, (int) def, range.min(), range.max(), Integer.class); + } else if (field.isAnnotationPresent(Config.RangeDouble.class) && (type == Double.class || type == double.class)) { + Config.RangeDouble range = field.getAnnotation(Config.RangeDouble.class); + value = builder.defineInRange(path, (double) def, range.min(), range.max(), Double.class); + } else if (Enum.class.isAssignableFrom(type)) { + if (!hasTooltip) { + StringBuilder b = new StringBuilder().append("Allowed values: "); + Class> enumClass = (Class>) type; + for (Enum e : enumClass.getEnumConstants()) { + b.append(e.name()).append(", "); + } + b.deleteCharAt(b.length() - 1); + b.deleteCharAt(b.length() - 1); + appendTranslation(path, b.toString(), true); + hasTooltip = true; + } + value = builder.defineEnum(path, (Enum) def); + } else if (type.isArray()) { + List list = new ArrayList<>(); + Collections.addAll(list, (Object[]) def); + value = builder.defineList(path, list, o -> true); + converter = o -> ((List) o).toArray((Object[]) Array.newInstance(type.getComponentType(), 0)); + appendTranslation(path + ".button", "Edit list", false); + } else if (def instanceof Boolean b) { + value = builder.define(path, b.booleanValue()); + } else { + value = builder.define(path, def); + } + if (!hasTooltip) appendTranslation(path, "", true); + return new Value(field, value, converter); + } + + public static void load() { + for (Value value : configs) { + value.apply(); + } + if (!translations.isEmpty()) { + // TODO use data gen + StringBuilder b = new StringBuilder("Config translations:\n"); + translations.forEach(b::append); + NEA.LOGGER.info(b); + } + } + + private static class Value { + + private Object instance; + private final Field field; + private final ModConfigSpec.ConfigValue configValue; + private final Function converter; + + private Value(Field field, ModConfigSpec.ConfigValue configValue, Function converter) { + this.field = field; + this.configValue = configValue; + this.converter = converter; + } + + private void apply() { + try { + field.set(instance, converter.apply(configValue.get())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/AbstractMixinPlugin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/AbstractMixinPlugin.java new file mode 100644 index 0000000..f7aba0f --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/AbstractMixinPlugin.java @@ -0,0 +1,62 @@ +package com.cleanroommc.neverenoughanimations.core; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +/** + * The whole purpose of this class is to not load mixins if its required mod is not loaded. + */ +public abstract class AbstractMixinPlugin implements IMixinConfigPlugin { + + private final boolean classLoaded; + + /** + * Creates a class which checks if a mod is loaded by checking if a specific class of the mod can be loaded. + * The given class should not be something, people may want to mixin to. + * + * @param modClassName mod class name to try to load + */ + public AbstractMixinPlugin(String modClassName) { + this.classLoaded = isClassFound(modClassName); + } + + @Override + public void onLoad(String mixinPackage) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return classLoaded; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) {} + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {} + + public static boolean isClassFound(String className) { + try { + Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/LateMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/LateMixin.java deleted file mode 100644 index ae568a9..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/LateMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core; - -import com.cleanroommc.neverenoughanimations.Tags; -import net.minecraftforge.fml.common.Loader; -import zone.rong.mixinbooter.ILateMixinLoader; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -public class LateMixin implements ILateMixinLoader { - - private static final String[] mods = { - "trashslot", - "jei", - "mousetweaks", - "thermalexpansion" - }; - - @Override - public List getMixinConfigs() { - return Arrays.stream(mods) - .map(m -> "mixin." + Tags.MODID + "." + m + ".json") - .collect(Collectors.toList()); - } - - @Override - public boolean shouldMixinConfigQueue(String mixinConfig) { - return Loader.isModLoaded(mixinConfig.split("\\.")[2]); - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/MixinPlugins.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/MixinPlugins.java new file mode 100644 index 0000000..3801f4b --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/MixinPlugins.java @@ -0,0 +1,22 @@ +package com.cleanroommc.neverenoughanimations.core; + +/** + * This class holds mixin plugin class which are defined in their respective mixin config. + * These classes are instantiated by Mixin. + */ +public class MixinPlugins { + + public static class MouseTweaks extends AbstractMixinPlugin { + + public MouseTweaks() { + super("yalter.mousetweaks.Logger"); + } + } + + public static class Jei extends AbstractMixinPlugin { + + public Jei() { + super("mezz.jei.api.JeiPlugin"); + } + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/NEACore.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/NEACore.java deleted file mode 100644 index f6c2207..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/NEACore.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core; - -import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; -import org.jetbrains.annotations.Nullable; -import zone.rong.mixinbooter.IEarlyMixinLoader; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class NEACore implements IFMLLoadingPlugin, IEarlyMixinLoader { - - @Override - public String[] getASMTransformerClass() { - return null; - } - - @Override - public String getModContainerClass() { - return null; - } - - @Nullable - @Override - public String getSetupClass() { - return null; - } - - @Override - public void injectData(Map data) { - } - - @Override - public String getAccessTransformerClass() { - return null; - } - - @Override - public List getMixinConfigs() { - return Collections.singletonList("mixin.neverenoughanimations.json"); - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerMenuMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerMenuMixin.java new file mode 100644 index 0000000..bef5d8e --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerMenuMixin.java @@ -0,0 +1,158 @@ +package com.cleanroommc.neverenoughanimations.core.mixin; + +import com.cleanroommc.neverenoughanimations.IItemLocation; +import com.cleanroommc.neverenoughanimations.NEA; +import com.cleanroommc.neverenoughanimations.NEAConfig; +import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; +import com.cleanroommc.neverenoughanimations.animations.ItemMovePacket; +import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; +import com.cleanroommc.neverenoughanimations.animations.SwapHolder; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.apache.commons.lang3.tuple.Pair; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(AbstractContainerMenu.class) +public abstract class AbstractContainerMenuMixin { + + @Shadow private int quickcraftStatus; + + @Shadow + public abstract ItemStack quickMoveStack(Player pPlayer, int pIndex); + + @Shadow + @Final + public NonNullList slots; + + @Shadow public abstract ItemStack getCarried(); + + @Unique + private List nea$getActualSlots() { + Screen screen = Minecraft.getInstance().screen; + return screen instanceof CreativeModeInventoryScreen gui ? gui.getMenu().slots : this.slots; + } + + @Inject(method = "doClick", at = @At("HEAD"), cancellable = true) + public void slotClick(int slotId, int button, ClickType clickTypeIn, Player player, CallbackInfo ci, + @Share("swapHolder") LocalRef swapHolder, + @Share("cursor") LocalRef cursor) { + if (player == null || !player.level().isClientSide()) return; + if (clickTypeIn == ClickType.QUICK_MOVE && (button == 0 || button == 1) && quickcraftStatus == 0 && slotId != -999) { + if (slotId < 0) { + ci.cancel(); + return; + } + List inventorySlots = nea$getActualSlots(); + Slot slot = inventorySlots.get(slotId); + if (slot == null || !slot.mayPickup(player) || !slot.hasItem()) { + ci.cancel(); + return; + } + ItemStack oldStack = slot.getItem().copy(); + // take snapshot of all current slots and its items where the item could land in + Pair, List> candidates = ItemMoveAnimation.getCandidates(slot, inventorySlots); + + // minecraft start + ItemStack itemstack8 = this.quickMoveStack(player, slotId); + while (!itemstack8.isEmpty() && ItemStack.isSameItem(slot.getItem(), itemstack8)) { + itemstack8 = this.quickMoveStack(player, slotId); + } + // minecraft end + + if (candidates != null) ItemMoveAnimation.handleMove(slot, oldStack, candidates); + ci.cancel(); + } else if (clickTypeIn == ClickType.SWAP && button >= 0 && button < 9) { + // fuck creative inventory + //if ((Object) this instanceof GuiContainerCreative.ContainerCreative || NEAConfig.moveAnimationTime == 0) return; + Slot targetSlot = nea$getActualSlots().get(slotId); + if (SwapHolder.INSTANCE.init(targetSlot, nea$getActualSlots(), button)) { + swapHolder.set(SwapHolder.INSTANCE); + } + } else if(clickTypeIn == ClickType.PICKUP_ALL && slotId >= 0) { + // prepare pickup all + if (NEAConfig.moveAnimationTime == 0) return; + cursor.set(getCarried().copy()); + } + } + + @Inject(method = "doClick", at = @At("TAIL")) + public void slotClickPost(int pSlotId, int pButton, ClickType pClickType, Player pPlayer, CallbackInfo ci, + @Share("swapHolder") LocalRef swapHolder) { + if (swapHolder.get() != null) { + swapHolder.get().performSwap(); + } + } + + @Redirect(method = "doClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;grow(I)V")) + public void pickupAllMid(ItemStack instance, int increment, @Share("packets") LocalRef> packets, + @Local(ordinal = 1) Slot slot) { + if (NEAConfig.moveAnimationTime > 0) { + // handle animation + if (packets.get() == null) packets.set(new Int2ObjectArrayMap<>()); + IItemLocation source = IItemLocation.of(slot); + ItemStack movingStack = instance.copy(); + movingStack.setCount(increment); + packets.get().put(source.nea$getSlotNumber(), new ItemMovePacket(NEA.time(), source, IItemLocation.CURSOR, movingStack)); + } + // do the redirected action + instance.grow(increment); + } + + @Inject(method = "doClick", at = @At("TAIL")) + public void pickupAllPost(int slotId, int button, ClickType clickType, Player player, CallbackInfo ci, + @Share("packets") LocalRef> packets, + @Share("cursor") LocalRef cursor) { + if (NEAConfig.moveAnimationTime == 0 || clickType != ClickType.PICKUP_ALL) return; + if (packets.get() != null && !packets.get().isEmpty()) { + for (var iterator = packets.get().int2ObjectEntrySet().fastIterator(); iterator.hasNext(); ) { + var e = iterator.next(); + ItemMoveAnimation.queueAnimation(e.getIntKey(), e.getValue()); + ItemMoveAnimation.updateVirtualStack(-1, cursor.get(), 1); + } + } + } + + @Inject(method = "doClick", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;drop(Lnet/minecraft/world/item/ItemStack;Z)Lnet/minecraft/world/entity/item/ItemEntity;", ordinal = 3)) + public void throwItem(int slotId, int button, ClickType clickType, Player player, CallbackInfo ci, + @Local(ordinal = 0) ItemStack throwing) { + if (NEAConfig.appearAnimationTime == 0) return; + IItemLocation slot = IItemLocation.of(nea$getActualSlots().get(slotId)); + if (slot.nea$getStack().isEmpty()) { + // only animate when shift is held (throw hole stack) or only one item is left + ItemPickupThrowAnimation.animate(slot.nea$getX(), slot.nea$getY(), throwing, false); + } + } + + @ModifyArg(method = "doClick", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;drop(Lnet/minecraft/world/item/ItemStack;Z)Lnet/minecraft/world/entity/item/ItemEntity;")) + public ItemStack animateThrow(ItemStack itemStackIn, @Local(ordinal = 0, argsOnly = true) int slot) { + if (NEAConfig.appearAnimationTime > 0 && slot == -999) { + ItemPickupThrowAnimation.animate(NEA.getMouseX() - 8, NEA.getMouseY() - 8, itemStackIn.copy(), true); + } + return itemStackIn; + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerScreenMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerScreenMixin.java new file mode 100644 index 0000000..986d29c --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/AbstractContainerScreenMixin.java @@ -0,0 +1,104 @@ +package com.cleanroommc.neverenoughanimations.core.mixin; + +import com.cleanroommc.neverenoughanimations.IItemLocation; +import com.cleanroommc.neverenoughanimations.NEA; +import com.cleanroommc.neverenoughanimations.NEAConfig; +import com.cleanroommc.neverenoughanimations.animations.ItemHoverAnimation; +import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; +import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; +import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +// lower priority than 1000 so that we are earlier than item borders mod +@Mixin(value = AbstractContainerScreen.class, priority = 900) +public abstract class AbstractContainerScreenMixin extends Screen { + + protected AbstractContainerScreenMixin(Component pTitle) { + super(pTitle); + } + + @Inject(method = "renderSlot", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V")) + public void injectVirtualStack(GuiGraphics pGuiGraphics, Slot pSlot, CallbackInfo ci, + @Local(ordinal = 0) LocalRef itemStack) { + if (NEAConfig.moveAnimationTime > 0) { + ItemStack virtualStack = ItemMoveAnimation.getVirtualStack((AbstractContainerScreen) (Object) this, pSlot); + if (virtualStack != null) { + itemStack.set(virtualStack); + } + } + } + + @Inject(method = "renderSlotContents", at = @At("HEAD")) + public void injectHoverScale(GuiGraphics graphics, ItemStack itemstack, Slot slot, String countString, CallbackInfo ci) { + if (NEAConfig.hoverAnimationTime > 0 && slot.isHighlightable()) { + graphics.pose().pushPose(); + float scale = ItemHoverAnimation.getRenderScale((AbstractContainerScreen) (Object) this, slot); + if (scale > 1f) { + int x = slot.x + 8; + int y = slot.y + 8; + graphics.pose().translate(x, y, 0); + graphics.pose().scale(scale, scale, 1); + graphics.pose().translate(-x, -y, 0); + } + } + } + + @Inject(method = "renderSlotContents", at = @At(value = "TAIL")) + public void endHoverScale(GuiGraphics graphics, ItemStack itemstack, Slot slot, String countString, CallbackInfo ci) { + if (NEAConfig.hoverAnimationTime > 0 && slot.isHighlightable()) { + // pop the stack here so that the item borders mod is not affected + graphics.pose().popPose(); + } + } + + @Inject(method = "renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;IIF)V", + at = @At("HEAD"), + cancellable = true) + public void dontDrawOverlay(GuiGraphics guiGraphics, Slot slot, int mouseX, int mouseY, float partialTick, CallbackInfo ci) { + if (!((Object) this instanceof CreativeModeInventoryScreen) && (!slot.isHighlightable() || !NEAConfig.itemHoverOverlay)) ci.cancel(); + } + + @Inject(method = "render", + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;popPose()V", shift = At.Shift.BEFORE)) + public void drawMovingItems(GuiGraphics pGuiGraphics, int pMouseX, int pMouseY, float pPartialTick, CallbackInfo ci) { + ItemPickupThrowAnimation.drawIndependentAnimations((AbstractContainerScreen) (Object) this, pGuiGraphics, font); + ItemMoveAnimation.drawAnimations(pGuiGraphics, font); + } + + @ModifyArg(method = "render", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderFloatingItem(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/item/ItemStack;IILjava/lang/String;)V", + ordinal = 0), + index = 1) + public ItemStack injectVirtualCursorStack(ItemStack stack) { + if (NEAConfig.moveAnimationTime > 0) { + ItemStack virtual = ItemMoveAnimation.getVirtualStack((AbstractContainerScreen) (Object) this, IItemLocation.CURSOR); + return virtual == null ? stack : virtual; + } + return stack; + } + + @Inject(method = "renderBackground", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderBg(Lnet/minecraft/client/gui/GuiGraphics;FII)V", + shift = At.Shift.BEFORE)) + public void injectRenderScale(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) { + NEA.drawScreenDebug(guiGraphics, (AbstractContainerScreen) (Object) this, mouseX, mouseY); + if (NEAConfig.moveAnimationTime > 0) { + OpeningAnimation.handleScale(guiGraphics, (AbstractContainerScreen) (Object) this, true); + } + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ContainerMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ContainerMixin.java deleted file mode 100644 index b2e3989..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ContainerMixin.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core.mixin; - -import com.cleanroommc.neverenoughanimations.IItemLocation; -import com.cleanroommc.neverenoughanimations.NEA; -import com.cleanroommc.neverenoughanimations.NEAConfig; -import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemMovePacket; -import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; -import com.cleanroommc.neverenoughanimations.animations.SwapHolder; -import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.Share; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.inventory.GuiContainerCreative; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.inventory.ClickType; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.ContainerPlayer; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import org.apache.commons.lang3.tuple.Pair; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.List; - -@Mixin(Container.class) -public abstract class ContainerMixin { - - @Shadow private int dragEvent; - - @Shadow public List inventorySlots; - - @Shadow - public abstract ItemStack transferStackInSlot(EntityPlayer playerIn, int index); - - @Inject(method = "slotClick", at = @At("HEAD"), cancellable = true) - public void slotClick(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("swapHolder") LocalRef swapHolder) { - if (player == null || player.world == null || !player.world.isRemote) return; - if (clickTypeIn == ClickType.QUICK_MOVE && (dragType == 0 || dragType == 1) && dragEvent == 0 && slotId != -999) { - if (slotId < 0) { - cir.setReturnValue(ItemStack.EMPTY); - return; - } - - Container c = (Container) (Object) this; - // creative gui does stuff very differently - List inventorySlots = c instanceof ContainerPlayer && - Minecraft.getMinecraft().currentScreen instanceof GuiContainerCreative gui ? - gui.inventorySlots.inventorySlots : this.inventorySlots; - Slot slot5 = inventorySlots.get(slotId); - - if (slot5 == null || !slot5.canTakeStack(player) || !slot5.getHasStack()) { - cir.setReturnValue(ItemStack.EMPTY); - return; - } - - ItemStack oldStack = slot5.getStack().copy(); - Pair, List> candidates = ItemMoveAnimation.getCandidates(slot5, inventorySlots); - ItemStack itemstack = ItemStack.EMPTY; - for (ItemStack itemstack7 = transferStackInSlot(player, slotId); !itemstack7.isEmpty() && ItemStack.areItemsEqual(slot5.getStack(), itemstack7); itemstack7 = this.transferStackInSlot(player, slotId)) { - itemstack = itemstack7.copy(); - } - if (candidates != null) ItemMoveAnimation.handleMove(slot5, oldStack, candidates); - cir.setReturnValue(itemstack); - } else if (clickTypeIn == ClickType.SWAP && dragType >= 0 && dragType < 9) { - // fuck creative inventory - if ((Object) this instanceof GuiContainerCreative.ContainerCreative || NEAConfig.moveAnimationTime == 0) return; - Slot targetSlot = this.inventorySlots.get(slotId); - if (SwapHolder.INSTANCE.init(targetSlot, this.inventorySlots, dragType)) { - swapHolder.set(SwapHolder.INSTANCE); - } - } - } - - @Inject(method = "slotClick", at = @At("TAIL")) - public void slotClickPost(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("swapHolder") LocalRef swapHolder) { - if (swapHolder.get() != null) { - swapHolder.get().performSwap(); - } - } - - @Inject(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/InventoryPlayer;getItemStack()Lnet/minecraft/item/ItemStack;", - ordinal = 13)) - public void pickupAllPre(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("cursor") LocalRef cursor) { - if (NEAConfig.moveAnimationTime == 0) return; - cursor.set(player.inventory.getItemStack().copy()); - } - - @Redirect(method = "slotClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;grow(I)V", ordinal = 2)) - public void pickupAllMid(ItemStack instance, int quantity, @Share("packets") LocalRef> packets, - @Local(ordinal = 1) Slot slot) { - if (NEAConfig.moveAnimationTime > 0) { - // handle animation - if (packets.get() == null) packets.set(new Int2ObjectArrayMap<>()); - IItemLocation source = IItemLocation.of(slot); - ItemStack movingStack = instance.copy(); - movingStack.setCount(quantity); - packets.get().put(source.nea$getSlotNumber(), new ItemMovePacket(NEA.time(), source, IItemLocation.CURSOR, movingStack)); - } - // do the redirected action - instance.grow(quantity); - } - - @Inject(method = "slotClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/inventory/Container;detectAndSendChanges()V")) - public void pickupAllPost(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("packets") LocalRef> packets, - @Share("cursor") LocalRef cursor) { - if (NEAConfig.moveAnimationTime == 0) return; - if (packets.get() != null && !packets.get().isEmpty()) { - for (var iterator = packets.get().int2ObjectEntrySet().fastIterator(); iterator.hasNext(); ) { - var e = iterator.next(); - ItemMoveAnimation.queueAnimation(e.getIntKey(), e.getValue()); - ItemMoveAnimation.updateVirtualStack(-1, cursor.get(), 1); - } - } - } - - @Inject(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/EntityPlayer;dropItem(Lnet/minecraft/item/ItemStack;Z)Lnet/minecraft/entity/item/EntityItem;", - ordinal = 3)) - public void throwItem(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Local(ordinal = 1) LocalRef throwing) { - if (NEAConfig.appearAnimationTime == 0) return; - IItemLocation slot = IItemLocation.of(this.inventorySlots.get(slotId)); - if (slot.nea$getStack().isEmpty()) { - // only animate when shift is held (throw hole stack) or only one item is left - ItemPickupThrowAnimation.animate(slot.nea$getX(), slot.nea$getY(), throwing.get(), false); - } - } - - @ModifyArg(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/EntityPlayer;dropItem(Lnet/minecraft/item/ItemStack;Z)Lnet/minecraft/entity/item/EntityItem;")) - public ItemStack animateThrow(ItemStack itemStackIn, @Local(ordinal = 0, argsOnly = true) int slot) { - if (NEAConfig.appearAnimationTime > 0 && slot == -999) { - ItemPickupThrowAnimation.animate(NEA.getMouseX() - 8, NEA.getMouseY() - 8, itemStackIn.copy(), true); - } - return itemStackIn; - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/CreativeSlotMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/CreativeSlotMixin.java deleted file mode 100644 index 974d5ea..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/CreativeSlotMixin.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core.mixin; - -import com.cleanroommc.neverenoughanimations.IItemLocation; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -@Mixin(targets = "net.minecraft.client.gui.inventory.GuiContainerCreative$CreativeSlot") -public abstract class CreativeSlotMixin extends Slot implements IItemLocation { - - @Shadow - @Final - private Slot slot; - - private CreativeSlotMixin(IInventory inventoryIn, int index, int xPosition, int yPosition) { - super(inventoryIn, index, xPosition, yPosition); - } - - @Override - public int nea$getX() { - return xPos; - } - - @Override - public int nea$getY() { - return yPos; - } - - @Override - public int nea$getSlotNumber() { - return slot.slotNumber; - } - - @Override - public ItemStack nea$getStack() { - return slot.getStack(); - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiContainerMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiContainerMixin.java deleted file mode 100644 index 72ec151..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiContainerMixin.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core.mixin; - -import com.cleanroommc.neverenoughanimations.IItemLocation; -import com.cleanroommc.neverenoughanimations.NEA; -import com.cleanroommc.neverenoughanimations.NEAConfig; -import com.cleanroommc.neverenoughanimations.animations.ItemHoverAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; -import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.Share; -import com.llamalad7.mixinextras.sugar.ref.LocalFloatRef; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(value = GuiContainer.class, priority = 950) -public class GuiContainerMixin extends GuiScreen { - - @Inject(method = "drawSlot", - at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;isEmpty()Z", ordinal = 5, shift = At.Shift.BEFORE)) - public void injectVirtualStack(Slot slotIn, CallbackInfo ci, @Local(ordinal = 0) LocalRef itemStack) { - if (NEAConfig.moveAnimationTime > 0) { - ItemStack virtualStack = ItemMoveAnimation.getVirtualStack((GuiContainer) (Object) this, slotIn); - if (virtualStack != null) { - itemStack.set(virtualStack); - } - } - } - - @Inject(method = "drawSlot", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/client/renderer/RenderItem;renderItemAndEffectIntoGUI(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/item/ItemStack;II)V", - shift = At.Shift.BEFORE)) - public void injectHoverScale(Slot slotIn, CallbackInfo ci, @Share("scale") LocalFloatRef scaleRef) { - if (NEAConfig.hoverAnimationTime > 0) { - GlStateManager.pushMatrix(); - float scale = ItemHoverAnimation.getRenderScale((GuiContainer) (Object) this, slotIn); - scaleRef.set(scale); - if (scale > 1f) { - int x = slotIn.xPos + 8; - int y = slotIn.yPos + 8; - GlStateManager.translate(x, y, 0); - GlStateManager.scale(scale, scale, 1); - GlStateManager.translate(-x, -y, 0); - } - } - } - - @Inject(method = "drawSlot", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/client/renderer/RenderItem;renderItemAndEffectIntoGUI(Lnet/minecraft/entity/EntityLivingBase;Lnet/minecraft/item/ItemStack;II)V", - shift = At.Shift.AFTER)) - public void midHoverScale(Slot slotIn, CallbackInfo ci) { - if (NEA.isItemBordersLoaded() && NEAConfig.hoverAnimationTime > 0) { - // itemborders wants to draw the borders now -> undo scale - GlStateManager.popMatrix(); - } - } - - - @Inject(method = "drawSlot", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/client/renderer/RenderItem;renderItemOverlayIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", - shift = At.Shift.AFTER)) - public void endHoverScale(Slot slotIn, CallbackInfo ci, @Share("scale") LocalFloatRef scaleRef) { - if (NEAConfig.hoverAnimationTime == 0) return; - if (NEA.isItemBordersLoaded()) { - // itemborders did draw its borders -> reapply scale - GlStateManager.pushMatrix(); - float scale = scaleRef.get(); - if (scale > 1f) { - int x = slotIn.xPos + 8; - int y = slotIn.yPos + 8; - GlStateManager.translate(x, y, 0); - GlStateManager.scale(scale, scale, 1); - GlStateManager.translate(-x, -y, 0); - } - } - GlStateManager.popMatrix(); - } - - @Redirect(method = "drawScreen", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/inventory/GuiContainer;drawGradientRect(IIIIII)V")) - public void dontDrawOverlay(GuiContainer instance, int i1, int i2, int i3, int i4, int i5, int i6) { - if (NEAConfig.itemHoverOverlay) { - drawGradientRect(i1, i2, i3, i4, i5, i6); - } - } - - @Inject(method = "drawScreen", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;popMatrix()V", shift = At.Shift.BEFORE)) - public void drawMovingItems(int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { - zLevel = 200; - itemRender.zLevel = 200; - ItemPickupThrowAnimation.drawIndependentAnimations((GuiContainer) (Object) this, itemRender, fontRenderer); - ItemMoveAnimation.drawAnimations(itemRender, fontRenderer); - itemRender.zLevel = 0; - zLevel = 0; - } - - @ModifyArg(method = "drawScreen", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/client/gui/inventory/GuiContainer;drawItemStack(Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", - ordinal = 0), - index = 0) - public ItemStack injectVirtualCursorStack(ItemStack stack) { - if (NEAConfig.moveAnimationTime > 0) { - ItemStack virtual = ItemMoveAnimation.getVirtualStack((GuiContainer) (Object) this, IItemLocation.CURSOR); - return virtual == null ? stack : virtual; - } - return stack; - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiIngameMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiIngameMixin.java index 5d802d8..e7af589 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiIngameMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/GuiIngameMixin.java @@ -3,19 +3,20 @@ import com.cleanroommc.neverenoughanimations.animations.HotbarAnimation; import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiIngame; -import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.gui.GuiGraphics; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyArg; -@Mixin(GuiIngame.class) -public class GuiIngameMixin extends Gui { +@Mixin(Gui.class) +public class GuiIngameMixin { - @ModifyArg(method = "renderHotbar", - at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiIngame;drawTexturedModalRect(IIIIII)V", ordinal = 1), - index = 0) - public int renderCurrentItemMarker(int x, @Local(ordinal = 0, argsOnly = true) ScaledResolution sr) { - return HotbarAnimation.getX(sr); + @ModifyArg(method = "renderItemHotbar", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiGraphics;blitSprite(Lnet/minecraft/resources/ResourceLocation;IIII)V", + ordinal = 1), + index = 1) + public int renderCurrentItemMarker(int x, @Local(argsOnly = true) GuiGraphics graphics) { + return HotbarAnimation.getX(graphics); } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/InventoryPlayerMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/InventoryPlayerMixin.java index 008ad87..4d1bf6b 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/InventoryPlayerMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/InventoryPlayerMixin.java @@ -1,30 +1,22 @@ package com.cleanroommc.neverenoughanimations.core.mixin; import com.cleanroommc.neverenoughanimations.animations.HotbarAnimation; -import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.world.entity.player.Inventory; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -@Mixin(InventoryPlayer.class) +@Mixin(Inventory.class) public class InventoryPlayerMixin { - @Shadow public int currentItem; + @Inject(method = "swapPaint", at = @At("HEAD")) + public void preSwap(double direction, CallbackInfo ci) { + HotbarAnimation.preTick(); + } - @Inject(method = "changeCurrentItem", at = @At("HEAD"), cancellable = true) - public void animeCurrentItem(int direction, CallbackInfo ci) { - if (direction == 0) { - ci.cancel(); - return; - } - int dir = direction > 0 ? 1 : -1; - int old = currentItem; - currentItem -= dir; - if (currentItem < 0) currentItem += 9; - currentItem %= 9; - HotbarAnimation.animate(old, currentItem); - ci.cancel(); + @Inject(method = "swapPaint", at = @At("RETURN")) + public void postSwap(double direction, CallbackInfo ci) { + HotbarAnimation.postTick(); } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/MinecraftMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/MinecraftMixin.java new file mode 100644 index 0000000..22f607e --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/MinecraftMixin.java @@ -0,0 +1,38 @@ +package com.cleanroommc.neverenoughanimations.core.mixin; + +import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; +import net.minecraft.client.Minecraft; +import net.minecraft.client.MouseHandler; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.sounds.SoundManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + + @Shadow @Nullable public Screen screen; + + @Shadow @Final private SoundManager soundManager; + + @Shadow @Final public MouseHandler mouseHandler; + + // this is ugly, but since open event are no longer called on close and closing event cant be cancelled we need to use mixin + @Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/neoforged/neoforge/client/ClientHooks;clearGuiLayers(Lnet/minecraft/client/Minecraft;)V", shift = At.Shift.BEFORE), + cancellable = true) + public void setScreen(Screen guiScreen, CallbackInfo ci) { + if (OpeningAnimation.onGuiOpen(guiScreen, screen, () -> { + this.soundManager.resume(); + this.mouseHandler.grabMouse(); + })) { + ci.cancel(); + } + } + +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ScreenMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ScreenMixin.java new file mode 100644 index 0000000..269d40f --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/ScreenMixin.java @@ -0,0 +1,28 @@ +package com.cleanroommc.neverenoughanimations.core.mixin; + +import com.cleanroommc.neverenoughanimations.NEA; +import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Screen.class) +public class ScreenMixin { + + // applies value from open/close animation to alpha of transparent background + @Redirect(method = "renderTransparentBackground", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;fillGradient(IIIIII)V")) + public void injectAlpha(GuiGraphics instance, int x1, int y1, int x2, int y2, int colorFrom, int colorTo) { + if ((Object) this instanceof AbstractContainerScreen containerScreen) { + float val = OpeningAnimation.getValue(containerScreen); + if (val != 1f) { + colorFrom = NEA.withAlpha(colorFrom, val * NEA.getAlphaF(colorFrom)); + colorTo = NEA.withAlpha(colorTo, val * NEA.getAlphaF(colorTo)); + } + } + instance.fillGradient(x1, y1, x2, y2, colorFrom, colorTo); + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotMixin.java index df707ea..42a2dee 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotMixin.java @@ -1,39 +1,40 @@ package com.cleanroommc.neverenoughanimations.core.mixin; import com.cleanroommc.neverenoughanimations.IItemLocation; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @Mixin(Slot.class) public abstract class SlotMixin implements IItemLocation { - @Shadow public int xPos; + @Shadow public abstract ItemStack getItem(); - @Shadow public int yPos; + @Shadow @Final public int x; - @Shadow public int slotNumber; + @Shadow @Final public int y; - @Shadow public abstract ItemStack getStack(); + @Shadow public int index; @Override public int nea$getX() { - return xPos; + return x; } @Override public int nea$getY() { - return yPos; + return y; } @Override public int nea$getSlotNumber() { - return slotNumber; + return index; } @Override public ItemStack nea$getStack() { - return getStack(); + return getItem(); } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotWrapperMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotWrapperMixin.java new file mode 100644 index 0000000..fe10acd --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/SlotWrapperMixin.java @@ -0,0 +1,42 @@ +package com.cleanroommc.neverenoughanimations.core.mixin; + +import com.cleanroommc.neverenoughanimations.IItemLocation; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(targets = "net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen$SlotWrapper") +public abstract class SlotWrapperMixin extends Slot implements IItemLocation { + + @Shadow @Final Slot target; + + @Shadow public abstract @NotNull ItemStack getItem(); + + public SlotWrapperMixin(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public int nea$getX() { + return x; + } + + @Override + public int nea$getY() { + return y; + } + + @Override + public int nea$getSlotNumber() { + return target.index; + } + + @Override + public ItemStack nea$getStack() { + return getItem(); + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/BookmarkOverlayMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/BookmarkOverlayMixin.java new file mode 100644 index 0000000..d7fbf0e --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/BookmarkOverlayMixin.java @@ -0,0 +1,39 @@ +package com.cleanroommc.neverenoughanimations.core.mixin.jei; + +import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; +import mezz.jei.gui.overlay.IngredientGridWithNavigation; +import mezz.jei.gui.overlay.bookmarks.BookmarkOverlay; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = BookmarkOverlay.class, remap = false) +public abstract class BookmarkOverlayMixin { + + @Shadow + @Final + private IngredientGridWithNavigation contents; + + @Inject(method = "drawScreen", at = @At("HEAD")) + public void drawScreenPre(Minecraft minecraft, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { + guiGraphics.pose().pushPose(); + Screen screen = Minecraft.getInstance().screen; + if (screen instanceof AbstractContainerScreen container) { + float val = 1f - OpeningAnimation.getValue(container); + if (val <= 0f) return; + guiGraphics.pose().translate(-contents.getBackgroundArea().width() * val, 0, 0); + } + } + + @Inject(method = "drawScreen", at = @At("TAIL")) + public void drawScreenPost(Minecraft minecraft, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { + guiGraphics.pose().popPose(); + } +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/IngredientListOverlayMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/IngredientListOverlayMixin.java index f48dd74..1253fee 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/IngredientListOverlayMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/IngredientListOverlayMixin.java @@ -1,49 +1,37 @@ package com.cleanroommc.neverenoughanimations.core.mixin.jei; -import com.cleanroommc.neverenoughanimations.NEA; import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; -import mezz.jei.api.gui.IGuiProperties; -import mezz.jei.config.Config; +import mezz.jei.gui.overlay.IngredientGridWithNavigation; import mezz.jei.gui.overlay.IngredientListOverlay; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import javax.annotation.Nullable; -import java.awt.*; - @Mixin(value = IngredientListOverlay.class, remap = false) public class IngredientListOverlayMixin { - @Shadow - @Nullable - private IGuiProperties guiProperties; - - @Shadow private Rectangle displayArea; + @Shadow @Final private IngredientGridWithNavigation contents; @Inject(method = "drawScreen", at = @At("HEAD")) - public void drawScreenPre(Minecraft minecraft, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { - if (guiProperties != null && (!NEA.isHeiLoaded() || !Config.bufferIngredientRenders())) { - GlStateManager.pushMatrix(); - GuiScreen screen = Minecraft.getMinecraft().currentScreen; - if (screen instanceof GuiContainer container) { - float val = 1f - OpeningAnimation.getValue(container); - if (val <= 0f) return; - GlStateManager.translate(displayArea.width * val, 0, 0); - } + public void drawScreenPre(Minecraft minecraft, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { + guiGraphics.pose().pushPose(); + Screen screen = Minecraft.getInstance().screen; + if (screen instanceof AbstractContainerScreen container) { + float val = 1f - OpeningAnimation.getValue(container); + if (val <= 0f) return; + guiGraphics.pose().translate(contents.getBackgroundArea().width() * val, 0, 0); } } @Inject(method = "drawScreen", at = @At("TAIL")) - public void drawScreenPost(Minecraft minecraft, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { - if (guiProperties != null && (!NEA.isHeiLoaded() || !Config.bufferIngredientRenders())) { - GlStateManager.popMatrix(); - } + public void drawScreenPost(Minecraft minecraft, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { + guiGraphics.pose().popPose(); } } diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/LeftAreaDispatcherMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/LeftAreaDispatcherMixin.java deleted file mode 100644 index 66de80d..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/jei/LeftAreaDispatcherMixin.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core.mixin.jei; - -import com.cleanroommc.neverenoughanimations.NEA; -import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; -import mezz.jei.config.Config; -import mezz.jei.gui.overlay.bookmarks.LeftAreaDispatcher; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.inventory.GuiContainer; -import net.minecraft.client.renderer.GlStateManager; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.awt.*; - -@Mixin(value = LeftAreaDispatcher.class, remap = false) -public abstract class LeftAreaDispatcherMixin { - - @Shadow private Rectangle displayArea; - - @Shadow private boolean canShow; - - @Shadow protected abstract boolean hasContent(); - - @Inject(method = "drawScreen", at = @At("HEAD")) - public void drawScreenPre(Minecraft minecraft, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { - if (canShow && hasContent() && (!NEA.isHeiLoaded() || !Config.bufferIngredientRenders())) { - GlStateManager.pushMatrix(); - GuiScreen screen = Minecraft.getMinecraft().currentScreen; - if (screen instanceof GuiContainer container) { - float val = 1f - OpeningAnimation.getValue(container); - if (val <= 0f) return; - GlStateManager.translate(-displayArea.width * val, 0, 0); - } - } - } - - @Inject(method = "drawScreen", at = @At("TAIL")) - public void drawScreenPost(Minecraft minecraft, int mouseX, int mouseY, float partialTicks, CallbackInfo ci) { - if (canShow && hasContent() && (!NEA.isHeiLoaded() || !Config.bufferIngredientRenders())) { - GlStateManager.popMatrix(); - } - } -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/mousetweaks/MainMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/mousetweaks/MainMixin.java index d51a012..68f59da 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/mousetweaks/MainMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/mousetweaks/MainMixin.java @@ -1,20 +1,20 @@ package com.cleanroommc.neverenoughanimations.core.mixin.mousetweaks; import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; -import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.Share; import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; import org.apache.commons.lang3.tuple.Pair; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import yalter.mousetweaks.IGuiScreenHandler; import yalter.mousetweaks.Main; -import yalter.mousetweaks.impl.IGuiScreenHandler; import java.util.List; @@ -24,32 +24,45 @@ public abstract class MainMixin { @Shadow private static IGuiScreenHandler handler; @Shadow - private static Slot findWheelApplicableSlot(List slots, Slot selectedSlot, boolean pushItems) { + private static Slot findPullSlot(List slots, Slot selectedSlot) { return null; } - @Redirect(method = "handleWheel", at = @At(value = "INVOKE", target = "Lyalter/mousetweaks/Main;findWheelApplicableSlot(Ljava/util/List;Lnet/minecraft/inventory/Slot;Z)Lnet/minecraft/inventory/Slot;")) - private static Slot handleWheelPre(List slots, Slot selectedSlot, boolean pushItems, - @Local(name = "originalStack") ItemStack stack, - @Share("candidates") LocalRef, List>> candidates, - @Share("sourceStack") LocalRef sourceStack, - @Share("sourceSlot") LocalRef sourceSlot) { - Slot applicableSlot = findWheelApplicableSlot(slots, selectedSlot, pushItems); - if (applicableSlot == null) return null; - if (pushItems) { - sourceStack.set(stack); - sourceSlot.set(selectedSlot); - candidates.set(ItemMoveAnimation.getCandidates(selectedSlot, handler.getSlots())); - } else { - sourceStack.set(applicableSlot.getStack().copy()); - sourceSlot.set(applicableSlot); - candidates.set(ItemMoveAnimation.getCandidates(applicableSlot, handler.getSlots())); - } - return applicableSlot; + @Shadow + private static List findPushSlots(List slots, Slot selectedSlot, int itemCount, boolean mustDistributeAll) { + return null; + } + + @Redirect(method = "onMouseScrolled", + at = @At(value = "INVOKE", + target = "Lyalter/mousetweaks/Main;findPullSlot(Ljava/util/List;Lnet/minecraft/world/inventory/Slot;)Lnet/minecraft/world/inventory/Slot;")) + private static Slot pullItems(List slots, Slot selectedSlot, + @Share("candidates") LocalRef, List>> candidates, + @Share("sourceStack") LocalRef sourceStack, @Share("sourceSlot") LocalRef sourceSlot) { + Slot pullSlot = findPullSlot(slots, selectedSlot); + if (pullSlot == null) return null; + sourceStack.set(pullSlot.getItem().copy()); + sourceSlot.set(pullSlot); + candidates.set(ItemMoveAnimation.getCandidates(pullSlot, handler.getSlots())); + return pullSlot; + } + + @Redirect(method = "onMouseScrolled", + at = @At(value = "INVOKE", + target = "Lyalter/mousetweaks/Main;findPushSlots(Ljava/util/List;Lnet/minecraft/world/inventory/Slot;IZ)Ljava/util/List;")) + private static List pushItems(List slots, Slot selectedSlot, int itemCount, boolean mustDistributeAll, + @Share("candidates") LocalRef, List>> candidates, + @Share("sourceStack") LocalRef sourceStack, + @Share("sourceSlot") LocalRef sourceSlot) { + List pushSlots = findPushSlots(slots, selectedSlot, itemCount, mustDistributeAll); + sourceStack.set(selectedSlot.getItem().copy()); + sourceSlot.set(selectedSlot); + candidates.set(ItemMoveAnimation.getCandidates(selectedSlot, handler.getSlots())); + return pushSlots; } - @Inject(method = "handleWheel", at = @At("RETURN")) - private static void handleWheelPost(Slot selectedSlot, CallbackInfo ci, + @Inject(method = "onMouseScrolled", at = @At("RETURN")) + private static void handleWheelPost(Screen screen, double x, double y, double scrollDelta, CallbackInfoReturnable cir, @Share("candidates") LocalRef, List>> candidates, @Share("sourceStack") LocalRef sourceStack, @Share("sourceSlot") LocalRef sourceSlot) { diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/thermal/ContainerInventoryItemMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/thermal/ContainerInventoryItemMixin.java deleted file mode 100644 index 6172228..0000000 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/thermal/ContainerInventoryItemMixin.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.cleanroommc.neverenoughanimations.core.mixin.thermal; - -import cofh.core.gui.container.ContainerInventoryItem; -import com.cleanroommc.neverenoughanimations.IItemLocation; -import com.cleanroommc.neverenoughanimations.NEA; -import com.cleanroommc.neverenoughanimations.NEAConfig; -import com.cleanroommc.neverenoughanimations.animations.ItemMoveAnimation; -import com.cleanroommc.neverenoughanimations.animations.ItemMovePacket; -import com.cleanroommc.neverenoughanimations.animations.ItemPickupThrowAnimation; -import com.cleanroommc.neverenoughanimations.animations.SwapHolder; -import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.Share; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.inventory.GuiContainerCreative; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.inventory.ClickType; -import net.minecraft.inventory.Container; -import net.minecraft.inventory.ContainerPlayer; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; -import org.apache.commons.lang3.tuple.Pair; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -import java.util.List; - -@Mixin(ContainerInventoryItem.class) -public class ContainerInventoryItemMixin { - - @Shadow(remap = false) - @Final - protected int containerIndex; - - @Inject(method = "slotClick", at = @At("HEAD"), cancellable = true) - public void slotClick(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("swapHolder") LocalRef swapHolder) { - if (clickTypeIn == ClickType.SWAP && dragType == this.containerIndex) return; - int dragEvent = ((Container) (Object) this).dragEvent; - List thisSlots = ((Container) (Object) this).inventorySlots; - if (player == null || player.world == null || !player.world.isRemote) return; - if (clickTypeIn == ClickType.QUICK_MOVE && (dragType == 0 || dragType == 1) && dragEvent == 0 && slotId != -999) { - if (slotId < 0) { - cir.setReturnValue(ItemStack.EMPTY); - return; - } - - Container c = (Container) (Object) this; - // creative gui does stuff very differently - List inventorySlots = - c instanceof ContainerPlayer && Minecraft.getMinecraft().currentScreen instanceof GuiContainerCreative gui ? - gui.inventorySlots.inventorySlots : thisSlots; - Slot slot5 = inventorySlots.get(slotId); - - if (slot5 == null || !slot5.canTakeStack(player) || !slot5.getHasStack()) { - cir.setReturnValue(ItemStack.EMPTY); - return; - } - - ItemStack oldStack = slot5.getStack().copy(); - Pair, List> candidates = ItemMoveAnimation.getCandidates(slot5, inventorySlots); - ItemStack itemstack = ItemStack.EMPTY; - for (ItemStack itemstack7 = ((Container) (Object) this).transferStackInSlot(player, slotId); !itemstack7.isEmpty() && - ItemStack.areItemsEqual(slot5.getStack(), itemstack7); itemstack7 = ((Container) (Object) this).transferStackInSlot( - player, slotId)) { - itemstack = itemstack7.copy(); - } - if (candidates != null) ItemMoveAnimation.handleMove(slot5, oldStack, candidates); - cir.setReturnValue(itemstack); - } else if (clickTypeIn == ClickType.SWAP && dragType >= 0 && dragType < 9) { - // fuck creative inventory - if ((Object) this instanceof GuiContainerCreative.ContainerCreative || NEAConfig.moveAnimationTime == 0) return; - Slot targetSlot = thisSlots.get(slotId); - if (SwapHolder.INSTANCE.init(targetSlot, thisSlots, dragType)) { - swapHolder.set(SwapHolder.INSTANCE); - } - } - } - - @Inject(method = "slotClick", at = @At("TAIL")) - public void slotClickPost(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("swapHolder") LocalRef swapHolder) { - if (swapHolder.get() != null) { - swapHolder.get().performSwap(); - } - } - - @Inject(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/InventoryPlayer;getItemStack()Lnet/minecraft/item/ItemStack;", - ordinal = 11)) - public void pickupAllPre(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("cursor") LocalRef cursor) { - if (NEAConfig.moveAnimationTime == 0) return; - cursor.set(player.inventory.getItemStack().copy()); - } - - @Redirect(method = "slotClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;grow(I)V", ordinal = 2)) - public void pickupAllMid(ItemStack instance, int quantity, @Share("packets") LocalRef> packets, - @Local(name = "slot3") Slot slot) { - if (NEAConfig.moveAnimationTime > 0) { - // handle animation - if (packets.get() == null) packets.set(new Int2ObjectArrayMap<>()); - IItemLocation source = IItemLocation.of(slot); - ItemStack movingStack = instance.copy(); - movingStack.setCount(quantity); - packets.get().put(source.nea$getSlotNumber(), new ItemMovePacket(NEA.time(), source, IItemLocation.CURSOR, movingStack)); - } - // do the redirected action - instance.grow(quantity); - } - - @Inject(method = "slotClick", - at = @At(value = "INVOKE", target = "Lcofh/core/gui/container/ContainerInventoryItem;detectAndSendChanges()V")) - public void pickupAllPost(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Share("packets") LocalRef> packets, - @Share("cursor") LocalRef cursor) { - if (NEAConfig.moveAnimationTime == 0) return; - if (packets.get() != null && !packets.get().isEmpty()) { - for (var iterator = packets.get().int2ObjectEntrySet().fastIterator(); iterator.hasNext(); ) { - var e = iterator.next(); - ItemMoveAnimation.queueAnimation(e.getIntKey(), e.getValue()); - ItemMoveAnimation.updateVirtualStack(-1, cursor.get(), 1); - } - } - } - - @Inject(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/EntityPlayer;dropItem(Lnet/minecraft/item/ItemStack;Z)Lnet/minecraft/entity/item/EntityItem;", - ordinal = 2)) - public void throwItem(int slotId, int dragType, ClickType clickTypeIn, EntityPlayer player, CallbackInfoReturnable cir, - @Local(name = "itemstack3") LocalRef throwing) { - if (NEAConfig.appearAnimationTime == 0) return; - IItemLocation slot = IItemLocation.of(((Container) (Object) this).inventorySlots.get(slotId)); - if (slot.nea$getStack().isEmpty()) { - // only animate when shift is held (throw hole stack) or only one item is left - ItemPickupThrowAnimation.animate(slot.nea$getX(), slot.nea$getY(), throwing.get(), false); - } - } - - @ModifyArg(method = "slotClick", - at = @At(value = "INVOKE", - target = "Lnet/minecraft/entity/player/EntityPlayer;dropItem(Lnet/minecraft/item/ItemStack;Z)Lnet/minecraft/entity/item/EntityItem;")) - public ItemStack animateThrow(ItemStack itemStackIn, @Local(ordinal = 0, argsOnly = true) int slot) { - if (NEAConfig.appearAnimationTime > 0 && slot == -999) { - ItemPickupThrowAnimation.animate(NEA.getMouseX() - 8, NEA.getMouseY() - 8, itemStackIn.copy(), true); - } - return itemStackIn; - } - -} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/trashslot/ClientProxyMixin.java b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/trashslot/ClientProxyMixin.java index efafb90..b6533a9 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/trashslot/ClientProxyMixin.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/core/mixin/trashslot/ClientProxyMixin.java @@ -1,4 +1,4 @@ -package com.cleanroommc.neverenoughanimations.core.mixin.trashslot; +/*package com.cleanroommc.neverenoughanimations.core.mixin.trashslot; import com.cleanroommc.neverenoughanimations.animations.OpeningAnimation; import net.blay09.mods.trashslot.client.ClientProxy; @@ -29,4 +29,4 @@ public void onDrawScreenPost(GuiScreenEvent.DrawScreenEvent.Pre event, CallbackI public void onDrawBackground(GuiScreenEvent.BackgroundDrawnEvent event, CallbackInfo ci) { OpeningAnimation.handleScale((GuiContainer) event.getGui(), false); } -} +}*/ diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/util/IStringSerializable.java b/src/main/java/com/cleanroommc/neverenoughanimations/util/IStringSerializable.java new file mode 100644 index 0000000..d921903 --- /dev/null +++ b/src/main/java/com/cleanroommc/neverenoughanimations/util/IStringSerializable.java @@ -0,0 +1,7 @@ +package com.cleanroommc.neverenoughanimations.util; + +public interface IStringSerializable { + + String getName(); + +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolation.java b/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolation.java index ba4c559..5d85574 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolation.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolation.java @@ -1,7 +1,5 @@ package com.cleanroommc.neverenoughanimations.util; -import net.minecraft.util.IStringSerializable; -import net.minecraft.util.math.MathHelper; import org.jetbrains.annotations.NotNull; /** @@ -271,7 +269,7 @@ public float interpolate(float a, float b, float x) { CIRCLE_IN("circle_in") { @Override public float interpolate(float a, float b, float x) { - x = MathHelper.clamp(x, 0, 1); + x = Math.clamp(x, 0, 1); float factor = 1 - (float) Math.sqrt(1 - Math.pow(x, 2)); @@ -281,7 +279,7 @@ public float interpolate(float a, float b, float x) { CIRCLE_OUT("circle_out") { @Override public float interpolate(float a, float b, float x) { - x = MathHelper.clamp(x, 0, 1); + x = Math.clamp(x, 0, 1); float factor = (float) Math.sqrt(1 - Math.pow(x - 1, 2)); @@ -291,7 +289,7 @@ public float interpolate(float a, float b, float x) { CIRCLE_INOUT("circle_inout") { @Override public float interpolate(float a, float b, float x) { - x = MathHelper.clamp(x, 0, 1); + x = Math.clamp(x, 0, 1); float factor = x < 0.5 ? (float) (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 @@ -311,4 +309,4 @@ public float interpolate(float a, float b, float x) { public @NotNull String getName() { return this.name; } -} \ No newline at end of file +} diff --git a/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolations.java b/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolations.java index d6f16ce..f61b144 100644 --- a/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolations.java +++ b/src/main/java/com/cleanroommc/neverenoughanimations/util/Interpolations.java @@ -1,12 +1,10 @@ package com.cleanroommc.neverenoughanimations.util; -import net.minecraft.util.math.MathHelper; - /** * Interpolation methods *

- * This class is responsible for doing different kind of interpolations. Cubic - * interpolation code was from website below, but BauerCam also uses this code. + * This class is responsible for doing different kind of interpolations. Cubic interpolation code was from website below, but BauerCam also + * uses this code. * * @author mchorse * @see Interpolations @@ -21,22 +19,20 @@ public static float lerp(float a, float b, float position) { } /** - * Special interpolation method for interpolating yaw. The problem with yaw, - * is that it may go in the "wrong" direction when having, for example, - * -170 (as a) and 170 (as b) degress or other way around (170 and -170). + * Special interpolation method for interpolating yaw. The problem with yaw, is that it may go in the "wrong" direction when having, for + * example, -170 (as a) and 170 (as b) degress or other way around (170 and -170). *

* This interpolation method fixes this problem. */ public static float lerpYaw(float a, float b, float position) { - a = MathHelper.wrapDegrees(a); - b = MathHelper.wrapDegrees(b); + a = wrapDegrees(a); + b = wrapDegrees(b); return lerp(a, normalizeYaw(a, b), position); } /** - * Cubic interpolation using Hermite between y1 and y2. Taken from paul's - * website. + * Cubic interpolation using Hermite between y1 and y2. Taken from paul's website. * * @param y0 - points[x-1] * @param y1 - points[x] @@ -56,10 +52,10 @@ public static double cubicHermite(double y0, double y1, double y2, double y3, do * Yaw normalization for cubic interpolation */ public static double cubicHermiteYaw(float y0, float y1, float y2, float y3, float position) { - y0 = MathHelper.wrapDegrees(y0); - y1 = MathHelper.wrapDegrees(y1); - y2 = MathHelper.wrapDegrees(y2); - y3 = MathHelper.wrapDegrees(y3); + y0 = wrapDegrees(y0); + y1 = wrapDegrees(y1); + y2 = wrapDegrees(y2); + y3 = wrapDegrees(y3); y1 = normalizeYaw(y0, y1); y2 = normalizeYaw(y1, y2); @@ -89,10 +85,10 @@ public static float cubic(float y0, float y1, float y2, float y3, float x) { * Yaw normalization for cubic interpolation */ public static float cubicYaw(float y0, float y1, float y2, float y3, float position) { - y0 = MathHelper.wrapDegrees(y0); - y1 = MathHelper.wrapDegrees(y1); - y2 = MathHelper.wrapDegrees(y2); - y3 = MathHelper.wrapDegrees(y3); + y0 = wrapDegrees(y0); + y1 = wrapDegrees(y1); + y2 = wrapDegrees(y2); + y3 = wrapDegrees(y3); y1 = normalizeYaw(y0, y1); y2 = normalizeYaw(y1, y2); @@ -102,8 +98,7 @@ public static float cubicYaw(float y0, float y1, float y2, float y3, float posit } /** - * Calculate X value for given T using some brute force algorithm... - * This method should be precise enough + * Calculate X value for given T using some brute force algorithm... This method should be precise enough * * @param x1 - control point of initial value * @param x2 - control point of final value @@ -130,8 +125,7 @@ public static float bezierX(float x1, float x2, float t, final float epsilon) { } /** - * Calculate X value for given T using default epsilon value. See - * other overload method for more information. + * Calculate X value for given T using default epsilon value. See other overload method for more information. */ public static float bezierX(float x1, float x2, float t) { return bezierX(x1, x2, t, 0.0005F); @@ -157,8 +151,7 @@ public static float bezier(float x1, float x2, float x3, float x4, float t) { } /** - * Normalize yaw rotation (argument {@code b}) based on the previous - * yaw rotation. + * Normalize yaw rotation (argument {@code b}) based on the previous yaw rotation. */ public static float normalizeYaw(float a, float b) { float diff = a - b; @@ -204,15 +197,14 @@ public static double lerp(double a, double b, double position) { } /** - * Special interpolation method for interpolating yaw. The problem with yaw, - * is that it may go in the "wrong" direction when having, for example, - * -170 (as a) and 170 (as b) degress or other way around (170 and -170). + * Special interpolation method for interpolating yaw. The problem with yaw, is that it may go in the "wrong" direction when having, for + * example, -170 (as a) and 170 (as b) degress or other way around (170 and -170). *

* This interpolation method fixes this problem. */ public static double lerpYaw(double a, double b, double position) { - a = MathHelper.wrapDegrees(a); - b = MathHelper.wrapDegrees(b); + a = wrapDegrees(a); + b = wrapDegrees(b); return lerp(a, normalizeYaw(a, b), position); } @@ -238,10 +230,10 @@ public static double cubic(double y0, double y1, double y2, double y3, double x) * Yaw normalization for cubic interpolation */ public static double cubicYaw(double y0, double y1, double y2, double y3, double position) { - y0 = MathHelper.wrapDegrees(y0); - y1 = MathHelper.wrapDegrees(y1); - y2 = MathHelper.wrapDegrees(y2); - y3 = MathHelper.wrapDegrees(y3); + y0 = wrapDegrees(y0); + y1 = wrapDegrees(y1); + y2 = wrapDegrees(y2); + y3 = wrapDegrees(y3); y1 = normalizeYaw(y0, y1); y2 = normalizeYaw(y1, y2); @@ -251,8 +243,7 @@ public static double cubicYaw(double y0, double y1, double y2, double y3, double } /** - * Calculate X value for given T using some brute force algorithm... - * This method should be precise enough + * Calculate X value for given T using some brute force algorithm... This method should be precise enough * * @param x1 - control point of initial value * @param x2 - control point of final value @@ -279,8 +270,7 @@ public static double bezierX(double x1, double x2, double t, final double epsilo } /** - * Calculate X value for given T using default epsilon value. See - * other overload method for more information. + * Calculate X value for given T using default epsilon value. See other overload method for more information. */ public static double bezierX(double x1, double x2, float t) { return bezierX(x1, x2, t, 0.0005F); @@ -306,8 +296,7 @@ public static double bezier(double x1, double x2, double x3, double x4, double t } /** - * Normalize yaw rotation (argument {@code b}) based on the previous - * yaw rotation. + * Normalize yaw rotation (argument {@code b}) based on the previous yaw rotation. */ public static double normalizeYaw(double a, double b) { double diff = a - b; @@ -342,4 +331,38 @@ public static double envelope(double x, double lowIn, double lowOut, double high return 1; } -} \ No newline at end of file + + /** + * the angle is reduced to an angle between -180 and +180 by mod, and a 360 check + */ + public static float wrapDegrees(float value) { + value = value % 360.0F; + + if (value >= 180.0F) { + value -= 360.0F; + } + + if (value < -180.0F) { + value += 360.0F; + } + + return value; + } + + /** + * the angle is reduced to an angle between -180 and +180 by mod, and a 360 check + */ + public static double wrapDegrees(double value) { + value = value % 360.0D; + + if (value >= 180.0D) { + value -= 360.0D; + } + + if (value < -180.0D) { + value += 360.0D; + } + + return value; + } +} diff --git a/src/main/resources/assets/neverenoughanimations/lang/en_us.json b/src/main/resources/assets/neverenoughanimations/lang/en_us.json new file mode 100644 index 0000000..cdc555e --- /dev/null +++ b/src/main/resources/assets/neverenoughanimations/lang/en_us.json @@ -0,0 +1,30 @@ +{ + "neverenoughanimations.configuration.title": "NeverEnoughAnimations Config", + "neverenoughanimations.configuration.section.neverenoughanimations.common.toml.title": "NeverEnoughAnimations Config", + "neverenoughanimations.configuration.section.neverenoughanimations.common.toml": "NeverEnoughAnimations Config", + "neverenoughanimations.configuration.hoverAnimationTime": "Hover animation time", + "neverenoughanimations.configuration.hoverAnimationTime.tooltip": "How many millieseconds it takes until an item is scaled to its full size on hover. 0 to disable.", + "neverenoughanimations.configuration.hoverAnimationCurve": "Hover animation easing curve", + "neverenoughanimations.configuration.hoverAnimationCurve.tooltip": "Allowed values: LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, EXP_IN, EXP_OUT, EXP_INOUT, BACK_IN, BACK_OUT, BACK_INOUT, ELASTIC_IN, ELASTIC_OUT, ELASTIC_INOUT, BOUNCE_IN, BOUNCE_OUT, BOUNCE_INOUT, SINE_IN, SINE_OUT, SINE_INOUT, QUART_IN, QUART_OUT, QUART_INOUT, QUINT_IN, QUINT_OUT, QUINT_INOUT, CIRCLE_IN, CIRCLE_OUT, CIRCLE_INOUT", + "neverenoughanimations.configuration.itemHoverOverlay": "Item hover overlay", + "neverenoughanimations.configuration.itemHoverOverlay.tooltip": "If the gray slot overlay (minecraft feature) should be rendered at all on hover. Default: false", + "neverenoughanimations.configuration.moveAnimationTime": "Item move animation time", + "neverenoughanimations.configuration.moveAnimationTime.tooltip": "How many millieseconds it takes until an item has moved to its target (activated on shift click). 0 to disable.", + "neverenoughanimations.configuration.moveAnimationCurve": "Item move animation easing curve", + "neverenoughanimations.configuration.moveAnimationCurve.tooltip": "Allowed values: LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, EXP_IN, EXP_OUT, EXP_INOUT, BACK_IN, BACK_OUT, BACK_INOUT, ELASTIC_IN, ELASTIC_OUT, ELASTIC_INOUT, BOUNCE_IN, BOUNCE_OUT, BOUNCE_INOUT, SINE_IN, SINE_OUT, SINE_INOUT, QUART_IN, QUART_OUT, QUART_INOUT, QUINT_IN, QUINT_OUT, QUINT_INOUT, CIRCLE_IN, CIRCLE_OUT, CIRCLE_INOUT", + "neverenoughanimations.configuration.appearAnimationTime": "Item (dis)appear animation time", + "neverenoughanimations.configuration.appearAnimationTime.tooltip": "How many millieseconds it takes until an item has moved to its target (activated on shift click). 0 to disable.", + "neverenoughanimations.configuration.appearAnimationCurve": "Item (dis)appear animation easing curve", + "neverenoughanimations.configuration.appearAnimationCurve.tooltip": "Allowed values: LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, EXP_IN, EXP_OUT, EXP_INOUT, BACK_IN, BACK_OUT, BACK_INOUT, ELASTIC_IN, ELASTIC_OUT, ELASTIC_INOUT, BOUNCE_IN, BOUNCE_OUT, BOUNCE_INOUT, SINE_IN, SINE_OUT, SINE_INOUT, QUART_IN, QUART_OUT, QUART_INOUT, QUINT_IN, QUINT_OUT, QUINT_INOUT, CIRCLE_IN, CIRCLE_OUT, CIRCLE_INOUT", + "neverenoughanimations.configuration.hotbarAnimationTime": "Hotbar animation time", + "neverenoughanimations.configuration.hotbarAnimationTime.tooltip": "How many millieseconds it takes until the current item marker in the hotbar moved to its new location. 0 to disable.", + "neverenoughanimations.configuration.hotbarAnimationCurve": "Hotbar animation easing curve", + "neverenoughanimations.configuration.hotbarAnimationCurve.tooltip": "Allowed values: LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, EXP_IN, EXP_OUT, EXP_INOUT, BACK_IN, BACK_OUT, BACK_INOUT, ELASTIC_IN, ELASTIC_OUT, ELASTIC_INOUT, BOUNCE_IN, BOUNCE_OUT, BOUNCE_INOUT, SINE_IN, SINE_OUT, SINE_INOUT, QUART_IN, QUART_OUT, QUART_INOUT, QUINT_IN, QUINT_OUT, QUINT_INOUT, CIRCLE_IN, CIRCLE_OUT, CIRCLE_INOUT", + "neverenoughanimations.configuration.openingAnimationTime": "Opening/Closing animation time", + "neverenoughanimations.configuration.openingAnimationTime.tooltip": "How many millieseconds it takes until the gui is fully opened. 0 to disable.", + "neverenoughanimations.configuration.openingAnimationCurve": "Opening/Closing animation easing curve", + "neverenoughanimations.configuration.openingAnimationCurve.tooltip": "Allowed values: LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, EXP_IN, EXP_OUT, EXP_INOUT, BACK_IN, BACK_OUT, BACK_INOUT, ELASTIC_IN, ELASTIC_OUT, ELASTIC_INOUT, BOUNCE_IN, BOUNCE_OUT, BOUNCE_INOUT, SINE_IN, SINE_OUT, SINE_INOUT, QUART_IN, QUART_OUT, QUART_INOUT, QUINT_IN, QUINT_OUT, QUINT_INOUT, CIRCLE_IN, CIRCLE_OUT, CIRCLE_INOUT", + "neverenoughanimations.configuration.guiAnimationBlacklist": "Gui class animation blacklist", + "neverenoughanimations.configuration.guiAnimationBlacklist.tooltip": "Add class names (works with * at the end) which should be blacklisted from any animations.\nThis is used to prevent visual issues with certain mods.", + "neverenoughanimations.configuration.guiAnimationBlacklist.button": "Edit list" +} diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info deleted file mode 100644 index 7c5b41c..0000000 --- a/src/main/resources/mcmod.info +++ /dev/null @@ -1,12 +0,0 @@ -[{ - "modid": "${modid}", - "name": "${modname}", - "description": "An example mod for Minecraft 1.12.2 with Forge", - "version": "${version}", - "mcversion": "${mcversion}", - "logoFile": "", - "url": "", - "authorList": [], - "credits": "", - "dependencies": [] -}] diff --git a/src/main/resources/mixin.neverenoughanimations.jei.json b/src/main/resources/mixin.neverenoughanimations.jei.json index b17bc67..8dff04f 100644 --- a/src/main/resources/mixin.neverenoughanimations.jei.json +++ b/src/main/resources/mixin.neverenoughanimations.jei.json @@ -1,13 +1,13 @@ { "package": "com.cleanroommc.neverenoughanimations.core.mixin.jei", "refmap": "mixins.neverenoughanimations.refmap.json", + "plugin": "com.cleanroommc.neverenoughanimations.core.MixinPlugins$Jei", "target": "@env(DEFAULT)", "minVersion": "0.8", "compatibilityLevel": "JAVA_8", "client": [ - "IngredientListOverlayMixin", - "LeftAreaDispatcherMixin" + "BookmarkOverlayMixin", + "IngredientListOverlayMixin" ], - "mixins": [ - ] + "mixins": [] } diff --git a/src/main/resources/mixin.neverenoughanimations.json b/src/main/resources/mixin.neverenoughanimations.json index 635fb9b..d609f5a 100644 --- a/src/main/resources/mixin.neverenoughanimations.json +++ b/src/main/resources/mixin.neverenoughanimations.json @@ -5,13 +5,14 @@ "minVersion": "0.8", "compatibilityLevel": "JAVA_8", "client": [ - "ContainerMixin", - "CreativeSlotMixin", - "GuiContainerMixin", + "AbstractContainerMenuMixin", + "AbstractContainerScreenMixin", "GuiIngameMixin", "InventoryPlayerMixin", - "SlotMixin" + "MinecraftMixin", + "ScreenMixin", + "SlotMixin", + "SlotWrapperMixin" ], - "mixins": [ - ] + "mixins": [] } diff --git a/src/main/resources/mixin.neverenoughanimations.mousetweaks.json b/src/main/resources/mixin.neverenoughanimations.mousetweaks.json index 2d89b03..7cf136c 100644 --- a/src/main/resources/mixin.neverenoughanimations.mousetweaks.json +++ b/src/main/resources/mixin.neverenoughanimations.mousetweaks.json @@ -1,12 +1,12 @@ { "package": "com.cleanroommc.neverenoughanimations.core.mixin.mousetweaks", "refmap": "mixins.neverenoughanimations.refmap.json", + "plugin": "com.cleanroommc.neverenoughanimations.core.MixinPlugins$MouseTweaks", "target": "@env(DEFAULT)", "minVersion": "0.8", "compatibilityLevel": "JAVA_8", "client": [ "MainMixin" ], - "mixins": [ - ] + "mixins": [] } diff --git a/src/main/resources/mixin.neverenoughanimations.thermalexpansion.json b/src/main/resources/mixin.neverenoughanimations.thermalexpansion.json deleted file mode 100644 index d87bb45..0000000 --- a/src/main/resources/mixin.neverenoughanimations.thermalexpansion.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "package": "com.cleanroommc.neverenoughanimations.core.mixin.thermal", - "refmap": "mixins.neverenoughanimations.refmap.json", - "target": "@env(DEFAULT)", - "minVersion": "0.8", - "compatibilityLevel": "JAVA_8", - "client": [ - "ContainerInventoryItemMixin" - ], - "mixins": [ - ] -} diff --git a/src/main/resources/mixin.neverenoughanimations.trashslot.json b/src/main/resources/mixin.neverenoughanimations.trashslot.json deleted file mode 100644 index 9e7269d..0000000 --- a/src/main/resources/mixin.neverenoughanimations.trashslot.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "package": "com.cleanroommc.neverenoughanimations.core.mixin.trashslot", - "refmap": "mixins.neverenoughanimations.refmap.json", - "target": "@env(DEFAULT)", - "minVersion": "0.8", - "compatibilityLevel": "JAVA_8", - "client": [ - "ClientProxyMixin" - ], - "mixins": [ - ] -} \ No newline at end of file diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..95ff61b --- /dev/null +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory +loaderVersion="${loader_version_range}" #mandatory +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 +modId="${mod_id}" #mandatory +version="${mod_version}" #mandatory +displayName="${mod_name}" #mandatory + +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional + +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="examplemod.png" #optional + +# A text field displayed in the mod UI +#credits="" #optional + +authors="${mod_authors}" #optional + +# The description text for the mod (multi line!) (#mandatory) +description='''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config="mixin.${mod_id}.json" + +[[mixins]] +config="mixin.${mod_id}.jei.json" + +[[mixins]] +config="mixin.${mod_id}.mousetweaks.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.${mod_id}]] #optional + # the modid of the dependency + modId="neoforge" #mandatory + # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). + # 'required' requires the mod to exist, 'optional' does not + # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning + type="required" #mandatory + # Optional field describing why the dependency is required or why it is incompatible + # reason="..." + # The version range of the dependency + versionRange="${neo_version_range}" #mandatory + # An ordering relationship for the dependency. + # BEFORE - This mod is loaded BEFORE the dependency + # AFTER - This mod is loaded AFTER the dependency + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT, or SERVER + side="BOTH" + +# Here's another dependency +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)"