Compare commits

...
26 Commits
Author SHA1 Message Date
LexManos 8ec8685098 Fix test detection in gradle 2026-11-12 13:25:41 -08:00
LexManos f375683bcd Fix Mod Sorting reporting incorrect cycles when sorting fails. Closes #10942
Co-authored-by: ZZZank <[email protected]>
2026-11-12 13:16:53 -08:00
LexManos 9cd61b99f1 Add junit tests for fmlloader Topological Sort
Co-authored-by: ZZZank <[email protected]>
2026-11-12 13:12:19 -08:00
Paint_Ninja ad5ca0b67d Deprecate more rarely used APIs for removal (#10907) 2026-11-02 14:21:52 -08:00
berkay8987 b76746e62f Fix missing forge config UI translation keys (#10909) 2026-11-02 14:10:58 -08:00
Paint_Ninja 4cfa1f83c4 Deprecate EventBusMigrationHelper for removal (#10908)
Migrating away from this should be trivial for mod devs. Refer to the PR for details.
2026-11-02 12:59:01 +00:00
LexManos da7e5f8bfa Fix EntityTeleportEvent.ChorusFruit is not fired Closes #10906 2026-10-29 12:17:44 -07:00
Paint_Ninja cd8eccc29c Deprecate some rarely used events for removal (#10904)
See PR description for details
2026-10-29 17:49:21 +00:00
LexManos 7d0bd1898f 26.3 RB 1 2026-10-21 11:02:26 -07:00
LexManos 55f03a2d86 Fix incorrect filter when placing items on bundles. Closes #10902 2026-10-16 13:40:27 -07:00
LexManos c328912074 Add protection against large network channel lists 2026-10-12 19:18:15 -07:00
BonsUnleashed aa4e31beab Store object holder handlers per registry name (#10899) 2026-10-12 13:18:32 -07:00
LexManos 4249a25ec7 Fix Spectator Mode having block overlay when inside blocks. Fixes #10900 2026-10-09 13:18:39 -07:00
LexManos 939fb13d3b Fix MDK including wrong resources. Fixes #10892 2026-09-30 14:11:21 -07:00
LexManos 932c6e9649 Fix checkLicense tasks with projectInfo task 2026-09-28 11:55:03 -07:00
LexManos ed6d9ff0e4 Move tests and mdk to sub-project (#10887)
This introduces two new artifacts
forge-examiner: A new mod locator that allows for a single directory to be split into multiple modules
forge-tests: Our test mods, showing examples of data gen, game tests, and various Forge features
Adds a new system useful for projects with multiple sub-projects that use datagen
2026-09-28 10:56:58 -07:00
Matt 851a57e808 Match ForgeRenderTypes OIT pipelines to vanilla, fixes #10890 (#10891) 2026-09-27 09:17:33 -07:00
LexManos 49aac2c496 Fix entities not being pushed by fluids 2026-09-26 18:32:39 -07:00
LexManos 26eeb1cc70 Fix typo in BlockScanUtils patch, fixes placing cushions. Closes #10889 2026-09-25 19:13:42 -07:00
LexManos 5afe3c050c Migrate to fully declarative ForgeGradlePlugin and local build-logic 2026-09-22 16:16:56 -07:00
LexManos 839fd66e52 Fix players dropping items. Closes #10885 2026-09-19 14:02:52 -07:00
LexManos fea644f6d2 Fix players not drowning propely 2026-09-19 14:02:21 -07:00
LexManos 2bc9649bd6 26.3 - Initial update 2026-09-19 07:45:38 -07:00
coolsim 41dfdc8f8b Fixed getToolModifiedState for AXE_SCRAPE (#10880) 2026-08-26 18:06:47 -07:00
RealMangoRage 4dae990880 Add TooltipContext and TooltipDisplay to ItemTooltipEvent 2026-08-18 12:34:10 -07:00
LexManos f0f144156b Fix mod packets being processed after vanilla packets when received in the same tick. 2026-08-10 21:56:47 -07:00
890 changed files with 8187 additions and 10773 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ name: Publish
on:
push:
branches: [ '26.2' ]
branches: [ '26.3' ]
permissions:
contents: read
+5 -6
View File
@@ -11,7 +11,7 @@ forgedev
/eclipse/
#idea
/.idea
**/.idea
/*/out
/classes
*.iml
@@ -33,13 +33,12 @@ forgedev
src/minecraft
#occupational hazards
/projects/**/build/
/projects/**/out/
/projects/**/run/
/projects/**/*.launch
/*/runs/
/repo/
/rejects-*/
src/*/generated/**/.cache/
**/src/*/generated/**/.cache/
**/*.launch
**/.project_info.properties
# Generated by gradle every import
**/src/main/resources/META-INF/MANIFEST.MF
+42
View File
@@ -0,0 +1,42 @@
plugins {
id("groovy-gradle-plugin")
alias(libs.plugins.licenser)
}
repositories {
mavenCentral()
gradlePluginPortal()
maven { url = 'https://maven.minecraftforge.net/' }
//mavenLocal()
}
dependencies {
compileOnly gradleApi()
// Manually import plugins so that we can apply them by id.
implementation plugin('licenser')
implementation plugin('gradleutils')
implementation plugin('gitversion')
implementation plugin('changelog')
implementation plugin('forgedev')
}
gradlePlugin {
plugins {
register('convention') {
id = 'net.minecraftforge.forge.build.convention'
implementationClass = 'net.minecraftforge.forge.build.ConventionPlugin'
}
}
}
final Provider<String> plugin(String alias) {
return (Provider<String>)project.extensions.getByType(VersionCatalogsExtension)
.named("libs")
.findPlugin(alias).get()
.map { it.pluginId + ":" + it.pluginId + ".gradle.plugin:" + it.version }
}
license {
header = file('../LICENSE-header.txt')
}
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
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=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
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 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" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@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 ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
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
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+30
View File
@@ -0,0 +1,30 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url = 'https://maven.minecraftforge.net/' }
mavenLocal()
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
//enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'
dependencyResolutionManagement.versionCatalogs {
register('bootLibs') {
description = 'Important version numbers that are externally declared for easy modification.'
from(files('../minecraft.versions.toml'))
}
create('libs') {
description = 'Libraries used by Forge and its subprojects.'
from(files("../gradle/libs.versions.toml"))
}
}
if (new File('forgedev/settings.gradle').exists())
includeBuild '../forgedev'
@@ -0,0 +1,166 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build
import groovy.transform.CompileStatic
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.ExternalModuleDependencyBundle
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.TaskProvider
import org.gradle.jvm.tasks.Jar
import javax.inject.Inject
@CompileStatic
abstract class ConventionExtension implements Meta {
private final Project project;
private final Runs runs
protected abstract @Inject ObjectFactory getObjects()
protected abstract @Inject ProviderFactory getProviders()
@Inject
ConventionExtension(Project project) {
this.project = project
this.runs = objects.newInstance(Runs, this)
}
@Override
Project getProject() {
this.project
}
void manifest(String pkg) {
this.project.tasks.named('jar', Jar).configure {
this.gradleutils.manifestDefaults(it.manifest, pkg)
}
}
void manifest(String pkg, Map<String, String> info) {
manifest(pkg)
this.project.tasks.named('jar', Jar).configure {
it.manifest.getAttributes().with {
for (var entry : info.entrySet())
put(entry.key, entry.value)
}
}
}
// region Maven publishing ==============================================================
void publish() {
publish(pub -> pub.from(project.components.getByName('java')))
}
void publish(Action<MavenPublication> config) {
project.with {
publishing.with {
repositories.with {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
// The actual artifacts to be published should be configured, so run the config
config.execute(it)
it.pom.with {
description = project.description
gradleutils.pom.addRemoteDetails(it)
license.with {
gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
}
}
// endregion
// region Runs ================================================================
private TaskProvider<Task> genAllData = null
TaskProvider<Task> getGenAllData() {
if (genAllData == null)
genAllData = this.project.tasks.register('genAllData')
return genAllData
}
TaskProvider<Task> getGenAllData(Action<Task> config) {
getGenAllData().configure(config)
return getGenAllData()
}
Runs getRuns() {
return this.runs
}
Runs runs(Action<Runs> config) {
config.execute(this.runs)
return this.runs
}
Runs basicRuns() {
return basicRuns({})
}
Runs basicRuns(Action<Runs> config) {
this.runs.main(null, null)
config.execute(this.runs)
return getRuns()
}
Runs basicRuns(String mods, File output) {
return basicRuns(mods, output, {})
}
Runs basicRuns(String mods, File output, Action<Runs> config) {
this.runs.main(mods, output)
config.execute(this.runs)
return getRuns()
}
Runs testRuns(String mods, File output) {
return testRuns(mods, output, {})
}
Runs testRuns(String mods, File output, Action<Runs> config) {
this.runs.test(mods, output)
config.execute(this.runs)
return getRuns()
}
// endregion
// region Forge Version Json ===================================================
private TaskProvider<WriteForgeVersionJson> writeForgeVersionJson = null
TaskProvider<WriteForgeVersionJson> getForgeVersionJson() {
if (writeForgeVersionJson == null) {
writeForgeVersionJson = this.project.tasks.register('writeForgeVersionJson', WriteForgeVersionJson)
final versions = this.versions
writeForgeVersionJson.configure {
it.forgeVersion.set(versions.forge)
it.minecraftVersion.set(versions.minecraft)
it.mcpVersion.set(versions.mcp)
}
}
return writeForgeVersionJson
}
TaskProvider<WriteForgeVersionJson> forgeVersionJson(Action<WriteForgeVersionJson> config) {
forgeVersionJson.configure(config)
return forgeVersionJson
}
// endregion
// region Natives helper =========================================================
// See gradle/gradle#35070 (https://github.com/gradle/gradle/issues/35070)
// We are making the list of providers first BEFORE mapping them to a provider of the resolved objects so
// that they can be finalized after configuration.
List<MinimalExternalModuleDependency> nativeVariants(Provider<ExternalModuleDependencyBundle> bundle) {
final natives = "natives-" + forgedev.os
final deps = this.project.dependencies
bundle.get().collect { dep ->
(deps.variantOf(providers.provider { (MinimalExternalModuleDependency)dep }) {
it.classifier(natives)
}).get()
}
}
// endregion
}
@@ -0,0 +1,96 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build
import groovy.transform.CompileStatic
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
@CompileStatic
class ConventionPlugin implements Plugin<Project>, Meta {
Project project
@Override
Project getProject() {
this.project
}
@Override
void apply(Project project) {
this.project = project
project.with {
pluginManager.with {
apply 'java-library'
apply 'maven-publish'
apply pluginId('licenser')
apply pluginId('gradleutils')
apply pluginId('gitversion')
apply pluginId('changelog')
apply pluginId('forgedev')
}
final versions = extensions.create("versions", VersionsExtension, project)
final convention = extensions.create('convention', ConventionExtension, project)
logger.lifecycle(name + " " + version)
gradleutils.with {
vendor.set('Forge Development LLC')
version.set(gitversion.version)
}
project.setGroup('net.minecraftforge')
java.with {
toolchain.languageVersion.set(JavaLanguageVersion.of(versions.java))
// This is needed for IDE runtime classpath to be correct/not have multiple of the same artifact
consistentResolution {
it.useCompileClasspathVersions()
}
// Forge needs to build its own sources jar due to patching, everything else should use the default
if (project != rootProject)
it.withSourcesJar()
}
tasks.withType(JavaCompile).configureEach {
it.options.with {
encoding = 'UTF-8'
compilerArgs << '-Xlint:-unchecked' // This is mainly for vanilla code, but just makes it quiet down outside the IDE
}
}
// NOTE: We are adding dependencies to projects instead of through settings, so we don't have to apply forgedev in settings.
// Applying forgedev in settings will break IDE linting support, which makes it very hard to work with.
// We can reconsider this if/when JetBrains gets their shit together.
// - I doubt they will ever fix their shit 8/28/2006 - Lex
repositories.with {
// Libraries has to be before maven central because Mojang hosts a classifer that central doesn't (org.lwjgl:lwjgl-freetype:3.3.3:natives-macos-patch)
maven { url = 'https://libraries.minecraft.net/' }
mavenCentral()
maven gradleutils.forgeMaven
project.pluginManager.withPlugin('net.minecraftforge.forgedev') {
maven forgedev.mavenizer
}
mavenLocal()
}
changelog.with {
from versions.changelogBase
}
license.with {
header = rootProject.file('LICENSE-header.txt')
exclude('.project_info.properties')
}
// Common dependencies,
project.dependencies.with {
add('compileOnly', libs.findLibrary('jetbrains.annotations').get())
}
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build
import groovy.transform.CompileStatic
import net.minecraftforge.forgedev.ForgeDevExtension
import net.minecraftforge.gitversion.gradle.GitVersionExtension
import net.minecraftforge.gitversion.gradle.changelog.ChangelogExtension
import net.minecraftforge.gradleutils.GradleUtilsExtensionForProject
import net.minecraftforge.licenser.LicenseExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.ExtraPropertiesExtension
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.publish.PublishingExtension
@CompileStatic
interface Meta {
Project getProject();
default VersionCatalog getLibs() {
project.extensions.getByType(VersionCatalogsExtension).named("libs")
}
default ExtraPropertiesExtension getExt() {
project.extensions.getByType(ExtraPropertiesExtension)
}
default JavaPluginExtension getJava() {
project.extensions.getByType(JavaPluginExtension)
}
default GradleUtilsExtensionForProject getGradleutils() {
project.extensions.getByType(GradleUtilsExtensionForProject)
}
default GitVersionExtension getGitversion() {
project.extensions.getByType(GitVersionExtension)
}
default ForgeDevExtension getForgedev() {
project.extensions.getByType(ForgeDevExtension)
}
default ChangelogExtension getChangelog() {
project.extensions.getByType(ChangelogExtension)
}
default LicenseExtension getLicense() {
project.extensions.getByType(LicenseExtension)
}
default PublishingExtension getPublishing() {
project.extensions.getByType(PublishingExtension)
}
default VersionsExtension getVersions() {
project.extensions.getByType(VersionsExtension)
}
default ConventionExtension getConvention() {
project.extensions.getByType(ConventionExtension)
}
default String pluginId(String alias) {
getLibs().findPlugin(alias).orElseThrow { new IllegalStateException("Could not find plugin " + alias) }.get().pluginId
}
}
@@ -0,0 +1,220 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build
import groovy.transform.CompileStatic
import net.minecraftforge.licenser.LicenseExtension
import net.minecraftforge.licenser.tasks.LicenseCheck
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.WriteProperties
import org.jspecify.annotations.Nullable
import javax.inject.Inject
import javax.naming.spi.ObjectFactory
@CompileStatic
class Runs implements Meta {
private final ConventionExtension ext
private boolean hasDependency = false
@Inject
Runs(ConventionExtension ext) {
this.ext = ext
}
@Override
Project getProject() {
this.ext.project
}
void main(@Nullable String mods, @Nullable File output) {
final project = this.project
final genAllData = this.ext.genAllData
project.with {
final SourceSet sourceSet = getJava().getSourceSets().named("main").get()
if (!hasDependency && project != project.rootProject) {
dependencies.with {
add('implementation', it.project(':')) // Root project is forge itself
}
hasDependency = true
}
if (output != null)
sourceSet.resources.with { srcDir output }
forgedev.runs.with {
final File existing = sourceSet.getResources().getSrcDirs()[0]
configureEach {
it.with {
cache.set(project.layout.projectDirectory.dir('runs/cache'))
eclipsePrefix = project.name
// If we're not forge, we need to get the metadata from the forge project
if (project != project.rootProject)
metadata = project.files(getForgedev().consumeFile('metadata', project.rootProject))
var runName = it.name.toLowerCase(Locale.ENGLISH)
if (runName.contains('client') || runName.contains('data')) {
options.with {
// LWJGL needs native access, Mojang gets away with just saying 'ALL-UNNAMED' because it doesnt boot into module land
// We need a system to translate that to module names
//jvmArgs '--enable-native-access=ALL-UNNAMED', '--add-exports', 'java.base/jdk.internal.misc=ALL-UNNAMED'
args '--assetsDir', '{assets_root}', '--assetIndex', '{asset_index}'
systemProperty 'org.lwjgl.system.SharedLibraryExtractDirectory', 'lwjgl_dll'
}
}
if (runName.contains('data')) {
options.with(sourceSet).with {
systemProperties.put("forge.project.list", getProjectList().flatMap { it.destinationFile.map{ it.asFile.absolutePath } })
if (mods != null) args('--mod', mods)
if (output != null) args('--output', output)
}
}
options.with {
getWorkingDir().set(project.layout.projectDirectory.dir("runs/$name"))
getMainClass().set('net.minecraftforge.bootstrap.ForgeBootstrap')
args '--gameDir', '.'
jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders', '-XX:StackShadowPages=32'
//systemProperty 'forge.logging.marker.registries', 'ACCEPT'
//systemProperty 'forge.logging.console.level', 'debug'
systemProperty 'bsl.debug', 'true'
systemProperty 'terminal.jline', 'true'
systemProperty 'forge.enableGameTest', 'true'
systemProperty 'eventbus.api.strictRuntimeChecks', 'true'
}
}
}
maybeCreate('client').with {
options.with {
args '--launchTarget', 'forge_dev_client',
'--username', 'Dev',
'--version', project.name,
'--accessToken', '0',
'--userType', 'mojang',
'--versionType', 'release'
}
}
maybeCreate('server').with {
options.with {
args '--launchTarget', 'forge_dev_server'
}
}
maybeCreate('gameTestServer').with {
options.with {
args '--launchTarget', 'forge_dev_server_gametest',
'--uniqueWorld' // Unique world is used so that the world regenerates, as well as the world config isn't influenced by other runs
}
}
maybeCreate('data').with {
options.with {
args '--launchTarget', 'forge_dev_data',
'--all',
'--existing', existing
}
}
maybeCreate('clientData').with {
options.with {
args '--launchTarget', 'forge_dev_client_data',
'--all',
'--existing', existing
}
}
}
genAllData.configure {
it.dependsOn forgedev.runs.getByName('data').with(sourceSet).run, forgedev.runs.getByName('clientData').with(sourceSet).run
}
}
}
void test(@Nullable String mods, @Nullable File output) {
final project = this.project
final genAllData = this.ext.genAllData
project.with {
final SourceSet sourceSet = getJava().getSourceSets().named("test").get()
forgedev.runs.with {
final File existing = sourceSet.getResources().getSrcDirs()[0]
configureEach {
it.with {
var runName = it.name.toLowerCase(Locale.ENGLISH)
options.with(sourceSet).with {
systemProperty 'forgedev.enableTestMods', 'true'
}
if (runName.contains('data')) {
options.with(sourceSet).with {
if (mods != null) args('--mod', mods)
if (output != null) args('--output', output)
args('--existing', existing)
}
}
}
}
}
genAllData.configure {
it.dependsOn forgedev.runs.getByName('data').with(sourceSet).run, forgedev.runs.getByName('clientData').with(sourceSet).run
}
}
}
private TaskProvider<WriteProperties> projectInfo = null
TaskProvider<WriteProperties> getProjectInfo() {
if (projectInfo == null) {
projectInfo = this.project.tasks.register("writeProjectInfo", WriteProperties)
projectInfo.configure {
it.destinationFile.set(this.project.file('src/main/resources/.project_info.properties'))
it.property('name', this.project.rootProject.name + this.project.isolated.path.replaceAll(':', '.'))
it.property('output', 'src/main/generated')
}
this.project.tasks.named("generateResources").configure { it.dependsOn(projectInfo) }
this.project.tasks.withType(LicenseCheck).configureEach {
it.dependsOn(projectInfo)
}
}
return projectInfo
}
TaskProvider<WriteProperties> projectInfo(Action<? extends WriteProperties> action) {
getProjectInfo().configure(action)
return getProjectInfo()
}
void setFlat(boolean value) {
projectInfo(task -> task.property('flat', value))
}
private TaskProvider<WriteProperties> projectList = null
TaskProvider<WriteProperties> getProjectList() {
if (projectList == null) {
projectList = this.project.tasks.register("writeProjectList", WriteProperties)
projectList.configure { task ->
task.destinationFile.set(this.project.layout.buildDirectory.file('project_list.properties'))
task.encoding = 'UTF-8'
final root = this.project.rootProject.name
this.project.rootProject.allprojects.forEach { p ->
task.property(root + p.isolated.path.replaceAll(':', '.'), p.isolated.projectDirectory.asFile.absolutePath)
}
}
}
return projectList
}
TaskProvider<WriteProperties> projectList(Action<? extends WriteProperties> action) {
getProjectList().configure(action)
return getProjectList()
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build;
import net.minecraftforge.gitversion.gradle.GitVersionExtension;
import org.gradle.api.Project;
import org.gradle.api.artifacts.VersionCatalog;
import org.gradle.api.artifacts.VersionCatalogsExtension;
import javax.inject.Inject;
public abstract class VersionsExtension {
private final VersionCatalog libs;
public final String java, minecraft, forge, forgeSpec, minecraftNext, mcp, changelogBase;
@Inject
public VersionsExtension(Project project) {
final var extensions = project.getExtensions();
final var gitversion = extensions.getByType(GitVersionExtension.class);
libs = extensions.getByType(VersionCatalogsExtension.class).named("bootLibs");
java = get("java");
minecraft = get("minecraft");
minecraftNext = get("minecraft.next");
mcp = get("mcp");
changelogBase = get("changelog.base");
forge = gitversion.of(project.getRootProject()).getVersion();
forgeSpec = forge.split("\\.")[0];
project.setVersion(minecraft + '-' + gitversion.getVersion());
}
private String get(String name) {
var ver = this.libs.findVersion(name);
if (ver.isEmpty())
throw new IllegalStateException("No version found for " + name);
return ver.get().toString();
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.build
import groovy.json.JsonBuilder
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import javax.inject.Inject
@CompileStatic
abstract class WriteForgeVersionJson extends DefaultTask {
abstract @Input Property<String> getForgeVersion()
abstract @Input Property<String> getMinecraftVersion()
abstract @Input Property<String> getMcpVersion()
abstract @OutputFile RegularFileProperty getOutputFile()
@Inject
WriteForgeVersionJson() {}
@TaskAction
@CompileDynamic
void exec() {
var json = new JsonBuilder()
json {
forge this.forgeVersion.get()
mc this.minecraftVersion.get()
mcp this.mcpVersion.get()
}
this.outputFile.asFile.get().text = json.toPrettyString()
}
}
+46 -251
View File
@@ -1,30 +1,16 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'java-library'
id 'idea'
id 'eclipse'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.versions
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
alias libs.plugins.jarsigner
id 'net.minecraftforge.forgedev'
alias libs.plugins.forgedev
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'Forge'
group = 'net.minecraftforge'
description = 'Modifications to Minecraft to enable mod developers.'
// NOTE: See settings.gradle for all the referenceable version variables.
// All subprojects (that apply 'net.minecraftforge.forge.build.convention') will have these available.
println "Version: $version"
java.toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
jarSigner.autoDetect('forge')
// We have to tell ForgeDev to use the exact versions of tools we ship in the installer
@@ -44,18 +30,19 @@ fdtools {
forgedev.validatePublish {
task {
baseBranch = 'upstream/26.1.2'
versionPrefix = minecraftVersion
versionPrefix = versions.minecraft
}
}
*/
final minecraftFiles = forgedev.minecraftFiles(minecraftVersion)
final minecraftFiles = forgedev.minecraftFiles(versions.minecraft)
final mcpBase = forgedev.mcpBase {
mcpVersion = "${minecraftVersion}-${project.ext.mcpVersion}"
mcpVersion = "${versions.minecraft}-${versions.mcp}"
accessTransformers.from file('src/main/resources/META-INF/accesstransformer.cfg')
sideAnnotationStrippers.from file('src/main/resources/forge.sas')
}
forgedev.provideFile('metadata', mcpBase.metadata)
forgedev.patches {
base = mcpBase
@@ -68,34 +55,12 @@ tasks.register('setup') {
dependsOn forgedev.patches.apply
}
sourceSets {
named('main') {
resources.srcDir 'src/main/generated'
}
named('test') {
resources.srcDir 'src/test/generated'
}
}
license {
header = file('LICENSE-header.txt')
include 'net/minecraftforge/'
exclude 'net/minecraftforge/common/LenientUnboundedMapCodec.java'
tasks.tap {
register('main') {
files.from files('src/main/java')
}
register('test') {
files.from files('src/test/java')
}
}
}
changelog {
from changelogBase
publishAll = false
}
@@ -117,10 +82,7 @@ configurations {
named('api') {
extendsFrom(installer)
}
named('implementation') {
// Inherit vanilla Minecraft's dependencies
// Inherit vanilla Minecraft's dependencies, and let downstream access them
extendsFrom(mcpBase.dependencyConfiguration)
}
}
@@ -171,106 +133,12 @@ dependencies {
annotationProcessor libs.eventbus.validator
}
convention.basicRuns('forge', file('src/main/generated/'))
final EXTRA_TXTS = [
file('LICENSE.txt'),
changelog.task.flatMap(task -> task.outputFile)
file('LICENSE.txt'),
changelog.task.flatMap(task -> task.outputFile)
]
forgedev.runs {
configureEach {
options {
workingDir = layout.projectDirectory.dir("runs/$name")
mainClass = 'net.minecraftforge.bootstrap.ForgeBootstrap'
args '--gameDir', '.'
jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders'
systemProperty 'bsl.debug', 'true'
systemProperty 'terminal.jline', 'true'
with(sourceSets.test) {
systemProperty 'forge.enableGameTest', 'true'
systemProperty 'forgedev.enableTestMods', 'true'
}
}
}
create('client') {
options {
systemProperty 'eventbus.api.strictRuntimeChecks', 'true'
systemProperty 'org.lwjgl.system.SharedLibraryExtractDirectory', 'lwjgl_dll'
args '--launchTarget', 'forge_dev_client',
'--username', 'Dev',
'--version', project.name,
'--accessToken', '0',
'--userType', 'mojang',
'--versionType', 'release',
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
}
}
create('server') {
options {
args '--launchTarget', 'forge_dev_server'
}
}
create('gameTestServer') {
options {
args '--launchTarget', 'forge_dev_server_gametest'
args '--uniqueWorld' // Unique world is used so that the world regenerates, as well as the world config isn't influenced by other runs
}
}
create('data') {
options {
args '--launchTarget', 'forge_dev_data',
'--all',
'--existing', sourceSets.main.resources.srcDirs[0],
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
with(sourceSets.main) {
args '--mod', 'forge',
'--output', rootProject.file('src/main/generated/')
}
with(sourceSets.test) {
args '--mod', '.+',
'--existing', sourceSets.test.resources.srcDirs[0],
'--output', rootProject.file('src/test/generated/')
}
}
}
create('clientData') {
options {
args '--all',
'--existing', sourceSets.main.resources.srcDirs[0],
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
with(sourceSets.main) {
args '--launchTarget', 'forge_dev_client_data',
'--mod', 'forge',
'--output', rootProject.file('src/main/generated/'),
'--mod', 'forge'
}
with(sourceSets.test) {
args '--launchTarget', 'forge_dev',
'--launchEntry', 'minecraft/net.minecraft.client.data.Main',
'--launchData',
'--mod', '.+',
'--existing', sourceSets.test.resources.srcDirs[0],
'--output', rootProject.file('src/test/generated/')
}
}
}
}
tasks.register('genAllData') {
dependsOn forgedev.runs.data.run, forgedev.runs.data.runTest, forgedev.runs.clientData.run, forgedev.runs.clientData.runTest
}
final serverShim = forgedev.shim {
config {
@@ -297,7 +165,7 @@ final finalizeSpawn = forgedev.methodCallFinder('findFinalizeSpawnTargets') {
}
forgedev.validateDeprecations(tasks.named('jar', Jar)) {
mcVersion = minecraftVersion
mcVersion = versions.minecraft
}
forgedev.checks() {
@@ -336,24 +204,15 @@ tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.forge'
'Automatic-Module-Name': 'net.minecraftforge.forge'
])
gradleutils.manifestDefaults(it, 'net/minecraftforge/versions/forge/')
gradleutils.manifestDefaults(it, 'net/minecraftforge/versions/mcp/', [
'Specification-Title' : 'Minecraft',
'Specification-Version' : versions.minecraft,
'Implementation-Title' : 'MCP',
'Implementation-Version': versions.mcp
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : gradleutils.vendor.get(),
'Specification-Version' : gitversion.info.tag,
'Implementation-Title' : project.group,
'Implementation-Vendor' : gradleutils.vendor.get(),
'Implementation-Version': forgeVersion
], 'net/minecraftforge/versions/forge/')
attributes([
'Specification-Title' : 'Minecraft',
'Specification-Vendor' : gradleutils.vendor.get(),
'Specification-Version' : minecraftVersion,
'Implementation-Title' : 'MCP',
'Implementation-Vendor' : gradleutils.vendor.get(),
'Implementation-Version': mcpVersion
], 'net/minecraftforge/versions/mcp/')
}
jarSigner.sign(it)
}
@@ -400,7 +259,7 @@ forgedev.userDev {
parents = []
main = 'net.minecraftforge.bootstrap.ForgeBootstrap'
args '--gameDir', '.'
jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders'
jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders', '-XX:StackShadowPages=32'
client = it.name.containsIgnoreCase('client')
environment 'MCP_MAPPINGS', '{mcp_mappings}'
}
@@ -445,7 +304,7 @@ forgedev.userDev {
}
}
forgedev.userdevCompatibility(minecraftVersion) {
forgedev.userdevCompatibility(versions.minecraft) {
clean = mcpBase.classes
dirty = tasks.named('jar', Jar)
}
@@ -454,8 +313,8 @@ forgedev.userdevCompatibility(minecraftVersion) {
final clientBinPatches = forgedev.binaryPatches('client') {
clean = minecraftFiles.client
// Workaround for 3rd party launcher issues, marker needed to locate MC, store needed to fix zlib-ng issues
apply {
store = true
apply {
store = true
marker = '.forge_patched_minecraft'
}
}
@@ -483,7 +342,7 @@ forgedev.installer {
launcherLibraries configurations.installer
final launcherId = "${minecraftVersion}-${project.name}-${forgeVersion}"
final launcherId = "${versions.minecraft}-${project.name}-${versions.forge}"
final installertools = tool(buildLibs.installertools)
final binarypatcher = tool(buildLibs.binarypatcher)
@@ -526,12 +385,12 @@ forgedev.installer {
json {
icon = rootProject.file('icon.ico')
executable serverShim.jar
minecraft = minecraftVersion
minecraft = versions.minecraft
profileVersion = launcherId
final projectArtifact = "${project.group}:${project.name}:${project.version}"
data 'MC_UNPACKED', "[net.minecraft:client:${minecraftVersion}]", "[net.minecraft:server:${minecraftVersion}:unpacked]"
data 'MC_UNPACKED', "[net.minecraft:client:${versions.minecraft}]", "[net.minecraft:server:${versions.minecraft}:unpacked]"
data 'MC_UNPACKED_SHA', minecraftFiles.client, minecraftFiles.serverExtracted
data 'BINPATCH', '/data/client.lzma', '/data/server.lzma'
@@ -592,57 +451,10 @@ forgedev.installer {
librariesLast = false // Makes libraries be added before args
id = launcherId
inheritsFrom = minecraftVersion
inheritsFrom = versions.minecraft
mainClass = 'net.minecraftforge.bootstrap.ForgeBootstrap'
gameArgs = [ '--launchTarget', 'forge_client' ]
jvmArgs = [ '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders' ]
}
}
final mdkGradleWrapper = tasks.register('mdkGradleWrapper', Wrapper) {
gradleVersion = '9.5.0'
scriptFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradlew")
jarFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradle/wrapper/gradle-wrapper.jar")
}
final mdkZip = tasks.register('mdkZip', Zip) {
dependsOn mdkGradleWrapper
archiveBaseName = project.name
archiveClassifier = 'mdk'
archiveVersion = project.version
destinationDirectory = file('build/libs')
from(EXTRA_TXTS)
from mdkGradleWrapper.map(Wrapper.&getScriptFile)
from mdkGradleWrapper.map(Wrapper.&getBatchScript)
into('gradle/wrapper/') {
from mdkGradleWrapper.map(Wrapper.&getJarFile)
from mdkGradleWrapper.map(Wrapper.&getPropertiesFile)
}
from(rootProject.file('mdk/')){
rootProject.file('mdk/gitignore.txt').eachLine {
if (!it.trim().isEmpty() && !it.trim().startsWith('#'))
exclude it
}
filter(ReplaceTokens, tokens: [
FORGE_VERSION: forgeVersion,
FORGE_GROUP: project.group,
FORGE_NAME: project.name,
MC_VERSION: minecraftVersion,
FORGE_SPEC_VERSION: gitversion.info.tag.split("\\.")[0],
MC_NEXT_VERSION: minecraftNextVersion,
EVENTBUS_VERSION: libs.versions.eventbus.get()
])
rename 'gitignore\\.txt', '.gitignore'
rename 'gitattributes\\.txt', '.gitattributes'
}
from(rootProject.file('src/test/java/com/example/examplemod/')) {
into('src/main/java/com/example/examplemod/')
}
from(rootProject.file('src/test/generated/mdk_datagen/')) {
into('src/main/resources/')
exclude '**/.cache/'
jvmArgs = [ '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders', '-XX:StackShadowPages=32' ]
}
}
@@ -710,8 +522,8 @@ tasks.named('javadoc', Javadoc) {
bottom = "Minecraft Forge is an open source modding API for Minecraft: Java Edition, licensed under the Lesser GNU General Public License, version 2.1."
windowTitle = "Minecraft Forge API ${version}"
docTitle = "Minecraft Forge API - ${forgeVersion} for Minecraft ${minecraftVersion}"
header = "<div style=\"margin-top: 9px;padding: 5px 6px;\"><strong>${forgeVersion} for Minecraft ${minecraftVersion}</strong></div>"
docTitle = "Minecraft Forge API - ${versions.forge} for Minecraft ${versions.minecraft}"
header = "<div style=\"margin-top: 9px;padding: 5px 6px;\"><strong>${versions.forge} for Minecraft ${versions.minecraft}</strong></div>"
}
doLast {
@@ -723,52 +535,36 @@ tasks.named('javadoc', Javadoc) {
}
}
*/
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
artifact universalJar
artifact forgedev.installer.jar
artifact mdkZip
artifact forgedev.userDev.jar
artifact sourcesJar
artifact serverShim.jar
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
final mdkZip = forgedev.consumeFile("mdkZip", project(':forge-mdk'))
convention.publish {
artifact universalJar
artifact forgedev.installer.jar
artifact(mdkZip.map { it.singleFile }) {
builtBy mdkZip
classifier = "mdk"
extension = "zip"
}
artifact forgedev.userDev.jar
artifact sourcesJar
artifact serverShim.jar
}
/*
// These are debugging tasks that are only used when developing ForgeDevPlugin, to bulk test task's config/caching
// region ForgeDev Tests
def all = tasks.register('all') {}
afterEvaluate {
if (gradle.startParameter.taskNames.contains('all')) {
tasks.forEach { task -> // This forces everything to be realized, which is why we have the startParameter check
if (!gradle.startParameter.isTaskGraph() && task.name.startsWith('run')) return
tasks.forEach { task -> // This forces everything to be realized, which is why we have the startParameter check
if (!gradle.startParameter.isTaskGraph() && task.name.startsWith('run')) return
if (task.name in [all.name, 'dependencyInsight', 'dependencies', 'dependencyUpdates', 'init', 'buildEnvironment', 'idea',
'eclipse', 'javaToolchains', 'outgoingVariants', 'projects', 'properties', 'resolvableConfigurations', 'tasks', 'updateDaemonJvm',
'generateActionsWorkflow'
]) return
if (task.name.startsWith('clean')) return
if (task.name == 'checkJarCompatibility') return // This doesn't work in FG6 dev, I think it's cuz we check vs srg names. Will need to look at later
all.configure { dependsOn(task) }
}
all.configure { dependsOn(task) }
}
}
}
*/
@@ -782,8 +578,7 @@ tasks.withType(AbstractTestTask).configureEach {
[ 'makePatches', 'compileJava', 'checkLicenseMain', 'updateLicenseMain', 'sourcesJar', 'sourcesJarAll' ]
.each{ tasks.named(it).configure{ mustRunAfter forgedev.patches.apply }}
forgedev.runs.configureEach {
run.configure { dependsOn finalizeSpawn}
runTest.configure { dependsOn finalizeSpawn}
run.configure { dependsOn finalizeSpawn }
}
// run gradlew dependencyUpdates --no-parallel
//noinspection UnnecessaryQualifiedReference
+3 -3
View File
@@ -2,8 +2,8 @@
MinecraftForge
=============
[![Stable Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Stable&prefix=26.2-&query=$.promos["26.2-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)][Download]
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=26.2-&query=$.promos["26.2-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
[![Stable Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Stable&prefix=26.3-&query=$.promos["26.3-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)][Download]
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=26.3-&query=$.promos["26.3-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
)][Download] [![Discord](https://img.shields.io/discord/313125603924639766.svg?color=%237289da&label=Discord&logo=discord&logoColor=%237289da)][Discord] [![Support](https://img.shields.io/badge/Patreon-Support-orange.svg?logo=Patreon)][Patreon]
Forge is a free, open-source modding API all of your favourite mods use!
@@ -74,7 +74,7 @@ Please ensure you have a valid email address associated with your GitHub account
### Thanks
[![YourKit](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/)
YourKit for providing us accesss to their [YourKit Java Profiler] which helps identify bottlenecks and places for improvment.
YourKit for providing us accesss to their [YourKit Java Profiler] which helps identify bottlenecks and places for improvement.
[Contributing]: ./CONTRIBUTING.md
[CLA]: https://cla-assistant.io/MinecraftForge/MinecraftForge
+6 -64
View File
@@ -1,78 +1,20 @@
plugins {
id 'java-library'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'FML'
final vendor = 'Forge Development LLC'
description = 'Modifications to Minecraft to enable mod developers.'
description = 'Core library used for Forge ModLoader.'
dependencies {
compileOnly libs.jetbrains.annotations
api libs.eventbus
implementation projects.fmlloader
implementation libs.commons.io
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
withSourcesJar()
}
convention.manifest('net/minecraftforge/fml/', [
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
'FMLModType' : 'LIBRARY'
])
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:-unchecked'
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
'FMLModType' : 'LIBRARY'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Version': '1.0',
'Implementation-Vendor' : vendor
], 'net/minecraftforge/fml/')
}
}
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
convention.publish()
@@ -16,7 +16,7 @@ import java.util.function.Supplier;
/**
* Used to allow access to certain things from the game layer
*/
public class Bindings {
public final class Bindings {
private Bindings() {}
/**
@@ -27,7 +27,7 @@ public class Bindings {
.findFirst().orElseThrow(() -> new IllegalStateException("Could not find bindings provider"));
/**
* @return A supplier of net.minecraftforge.common.MinecraftForge#EVENT_BUS
* @return A supplier of {@link BusGroup#DEFAULT}
*/
public static Supplier<BusGroup> getForgeBus() {
return PROVIDER.getForgeBusSupplier();
@@ -101,7 +101,7 @@ public final class ModList {
}
public static Optional<? extends ModContainer> getModContainerById(String modId) {
return Optional.ofNullable(indexedMods.get(modId));
return Optional.ofNullable(indexedMods == null ? null : indexedMods.get(modId));
}
public static Optional<? extends ModContainer> getModContainerByObject(Object obj) {
@@ -366,6 +366,10 @@ public final class ModLoader {
mod.acceptEvent(e);
}
public static <T extends IModBusEvent> void postEvent(ModContainer mod, T e) {
mod.acceptEvent(e);
}
public static <T extends IModBusEvent> T postEventWithReturn(T e) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
+6 -89
View File
@@ -1,49 +1,14 @@
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.internal.os.OperatingSystem
plugins {
id 'java-library'
id 'jvm-test-suite'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
id 'net.minecraftforge.forge.build.convention'
}
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = "natives-windows"
break
}
gradleutils.displayName = 'FML Early Display'
final vendor = 'Forge Development LLC'
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
dependencies {
compileOnly libs.jetbrains.annotations
implementation projects.fmlloader
implementation projects.fmlcore
implementation libs.bundles.lwjgl
@@ -51,67 +16,19 @@ dependencies {
implementation libs.jopt.simple
}
// See gradle/gradle#35070 (https://github.com/gradle/gradle/issues/35070)
// We are making the list of providers first BEFORE mapping them to a provider of the resolved objects so that they can be finalized after configuration.
var lwjglTestLibs = [
dependencies.variantOf(earlyDisplayTestLibs.lwjgl) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.glfw) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.opengl) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.stb) { classifier lwjglNatives }
].with { list ->
provider { list.collect { it.get() } }
}
testing.suites.named('test', JvmTestSuite) {
useJUnitJupiter('5.8.2')
dependencies {
implementation earlyDisplayTestLibs.powermock.core
runtimeOnly earlyDisplayLibs.slf4j.jdk14
runtimeOnly.bundle lwjglTestLibs
runtimeOnly.bundle convention.nativeVariants(earlyDisplayTestLibs.bundles.lwjgl)
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
convention.manifest('net/minecraftforge/fml/earlydisplay/', [
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer' : 'boot'
])
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer' : 'boot'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Vendor' : vendor,
'Implementation-Version': '1.0'
], 'net/minecraftforge/fml/earlydisplay/')
}
}
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
convention.publish()
@@ -13,10 +13,12 @@ import net.minecraftforge.fml.loading.ImmediateWindowHandler;
import net.minecraftforge.fml.loading.ImmediateWindowProvider;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLVideo;
import org.lwjgl.sdl.SDL_Event;
import org.lwjgl.sdl.SDL_Rect;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,12 +31,10 @@ import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -43,17 +43,18 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL.createCapabilities;
import static org.lwjgl.opengl.GL32C.*;
import static org.lwjgl.sdl.SDLError.*;
import static org.lwjgl.sdl.SDLEvents.*;
import static org.lwjgl.sdl.SDLHints.*;
import static org.lwjgl.sdl.SDLInit.*;
import static org.lwjgl.sdl.SDLVideo.*;
/**
* The Loading Window that is opened Immediately after Forge starts.
* It is called from the ModDirTransformerDiscoverer, the soonest method that ModLauncher calls into Forge code.
@@ -83,10 +84,10 @@ public class DisplayWindow implements ImmediateWindowProvider {
private ScheduledFuture<?> initializationFuture;
private PerformanceInfo performanceInfo;
@SuppressWarnings("unused")
private ScheduledFuture<?> performanceTick;
// The GL ID of the window. Used for all operations
private long window;
// The SDL Context we use for switching between threads
private long sdlContext;
// The thread that contains and ticks the window while Forge is loading mods
private static final ScheduledExecutorService renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
final var thread = Executors.defaultThreadFactory().newThread(r);
@@ -116,8 +117,8 @@ public class DisplayWindow implements ImmediateWindowProvider {
@Override
public ImmediateWindowProvider selectBackend(String backend) {
// We only support opengl
if ("default".equals(backend) || "opengl".equals(backend))
return this;
//if ("default".equals(backend) || "opengl".equals(backend))
// return this;
return ImmediateWindowProvider.getFallbackHandler();
}
@@ -184,7 +185,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
return;
}
nextFrameTime = nt + MINFRAMETIME;
glfwMakeContextCurrent(window);
SDL_GL_MakeCurrent(window, sdlContext);
framebuffer.activate();
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
this.context.elementShader().activate();
@@ -196,11 +197,12 @@ public class DisplayWindow implements ImmediateWindowProvider {
glViewport(0, 0, fbWidth, fbHeight);
framebuffer.draw(this.fbWidth, this.fbHeight);
// Swap buffers; we're done
glfwSwapBuffers(window);
SDL_GL_SwapWindow(window);
} catch (Throwable t) {
LOGGER.error("BARF", t);
} finally {
if (this.windowTick != null) glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread
if (this.windowTick != null)
SDL_GL_MakeCurrent(window, 0); // we release the gl context IF we're running off the main thread
renderLock.release();
}
}
@@ -212,12 +214,18 @@ public class DisplayWindow implements ImmediateWindowProvider {
* Nothing fancy, we just want to draw and render text.
*/
private void initRender(final @Nullable String mcVersion, final String forgeVersion) {
this.sdlContext = SDL_GL_CreateContext(window);
if (sdlContext == 0)
throw new IllegalStateException("Failed to create OpenGL context: " + SDL_GetError());
// This thread owns the GL render context now. We should make a note of that.
glfwMakeContextCurrent(window);
bindGlContext();
GL.createCapabilities();
// Wait for one frame to be complete before swapping; enable vsync in other words.
glfwSwapInterval(1);
createCapabilities();
LOGGER.info("GL info: "+ glGetString(GL_RENDERER) + " GL version " + glGetString(GL_VERSION) + ", " + glGetString(GL_VENDOR));
SDL_GL_SetSwapInterval(1);
LOGGER.info("GL info: " + glGetString(GL_RENDERER) + " GL version " + glGetString(GL_VERSION) + ", " + glGetString(GL_VENDOR));
elementShader = new ElementShader();
try {
@@ -227,7 +235,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
crashElegantly("An error occurred initializing shaders.");
}
// Set the clear color based on the colour scheme
// Set the clear color based on the color scheme
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
// we always render to an 854x480 texture and then fit that to the screen - with a scale factor
@@ -253,11 +261,15 @@ public class DisplayWindow implements ImmediateWindowProvider {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glfwMakeContextCurrent(0);
releaseGlContext();
this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS);
this.performanceTick = renderScheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS);
// Update the performance information every 1/2 second
renderScheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS);
// schedule a 50 ms ticker to try and smooth out the rendering
renderScheduler.scheduleAtFixedRate(()-> animationTimerTrigger.set(true), 1, 50, TimeUnit.MILLISECONDS);
//renderScheduler.scheduleAtFixedRate(() -> animationTimerTrigger.set(true), 1, 50, TimeUnit.MILLISECONDS);
}
/**
@@ -294,6 +306,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
glBindVertexArray(currentVAO);
glBindFramebuffer(GL_FRAMEBUFFER, currentFB);
}
/**
* Start the window and Render Thread; we're ready to go.
*/
@@ -342,71 +355,65 @@ public class DisplayWindow implements ImmediateWindowProvider {
* As long as we can verify that, then GL (and things like OS X) have no complaints with doing this.
*
* @param mcVersion Minecraft Version
* @return The selected GL profile as an integer pair
*/
public void initWindow(@Nullable String mcVersion) {
// Initialize GLFW with a time guard, in case something goes wrong
long glfwInitBegin = System.nanoTime();
if (!glfwInit()) {
crashElegantly("We are unable to initialize the graphics system.\nglfwInit failed.\n");
throw new IllegalStateException("Unable to initialize GLFW");
// Initialize a time guard, in case something goes wrong
long initBegin = System.nanoTime();
if (!SDL_Init(SDL_INIT_VIDEO)) {
crashElegantly("We are unable to initialize the graphics system.\nSDL_Init failed.\n");
throw new IllegalStateException("Unable to initialize SDL");
}
long glfwInitEnd = System.nanoTime();
long initEnd = System.nanoTime();
if (glfwInitEnd - glfwInitBegin > 1e9) {
LOGGER.error("WARNING : glfwInit took {} seconds to start.", (glfwInitEnd - glfwInitBegin) / 1.0e9);
}
// Clear the Last Exception (#7285 - Prevent Vanilla throwing an IllegalStateException due to invalid controller mappings)
handleLastGLFWError((error, description) -> LOGGER.error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description)));
if (initEnd - initBegin > 1e9)
LOGGER.error("WARNING : Window init took {} seconds to start.", (initEnd - initBegin) / 1.0e9);
// Set window hints for the new window we're gonna create.
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
SDL_ResetHints();
String vanillaWindowTitle = "Minecraft* ";
if (mcVersion != null) vanillaWindowTitle += mcVersion;
if (mcVersion != null)
vanillaWindowTitle += mcVersion;
// this emulates what we would get without early progress window
// as vanilla never sets these, so GLFW uses the first window title
// set them explicitly to avoid it using "FML early loading progress" as the class
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
long primaryMonitor = glfwGetPrimaryMonitor();
var primaryMonitor = SDL_GetPrimaryDisplay();
if (primaryMonitor == 0) {
LOGGER.error("Failed to find a primary monitor - this means LWJGL isn't working properly");
crashElegantly("Failed to locate a primary monitor.\nglfwGetPrimaryMonitor failed.\n");
crashElegantly("Failed to locate a primary monitor.\nSDL_GetPrimaryDisplay failed.\n");
throw new IllegalStateException("Can't find a primary monitor");
}
GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor);
var vidmode = SDL_GetDesktopDisplayMode(primaryMonitor);
if (vidmode == null) {
LOGGER.error("Failed to get the current display video mode.");
crashElegantly("Failed to get current display resolution.\nglfwGetVideoMode failed.\n");
crashElegantly("Failed to get current display resolution.\nSDL_GetDesktopDisplayMode failed.\n");
throw new IllegalStateException("Can't get a resolution");
}
long window = 0;
var successfulWindow = new AtomicBoolean(false);
var windowFailFuture = renderScheduler.schedule(()->{
if (!successfulWindow.get()) crashElegantly("Timed out trying to setup the Game Window.");
var windowFailFuture = renderScheduler.schedule(() -> {
if (!successfulWindow.get())
crashElegantly("Timed out trying to setup the Game Window.");
}, 10, TimeUnit.SECONDS);
int versidx = 0;
var skipVersions = FMLConfig.<String>getListConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SKIP_GL_VERSIONS);
boolean showHelpLog = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG);
final String[] lastGLError = new String[GL_VERSIONS.length];
do {
final var glVersionToTry = GL_VERSIONS[versidx][0] + "." + GL_VERSIONS[versidx][1];
if (skipVersions.contains(glVersionToTry)) {
LOGGER.info("Skipping GL version "+ glVersionToTry+" because of configuration");
versidx++;
record Attempt(String vesion, String error) {}
final List<Attempt> attempts = new ArrayList<>();
String requestedVersion = null;
for (int x = 0; x < GL_VERSIONS.length; x++) {
var version = GL_VERSIONS[x];
requestedVersion = version[0] + "." + version[1];
if (skipVersions.contains(requestedVersion)) {
LOGGER.info("Skipping GL version " + requestedVersion + " because of configuration");
continue;
}
LOGGER.info("Trying GL version " + glVersionToTry);
if (showHelpLog && versidx == 0) {
LOGGER.info("Trying GL version " + requestedVersion);
if (showHelpLog && x == 0) {
LOGGER.info("""
If this message is the only thing at the bottom of your log before a crash, you probably have a driver issue.
@@ -418,132 +425,105 @@ public class DisplayWindow implements ImmediateWindowProvider {
You can safely ignore this message if the game starts up successfully.""");
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSIONS[versidx][0]); // we try our versions one at a time
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSIONS[versidx][1]);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
window = glfwCreateWindow(winWidth, winHeight, vanillaWindowTitle, 0L, 0L);
var erridx = versidx;
handleLastGLFWError((error, description) -> lastGLError[erridx] = String.format("Trying %d.%d: GLFW error: [0x%X]%s", GL_VERSIONS[erridx][0], GL_VERSIONS[erridx][1], error, description));
if (lastGLError[versidx] != null) {
LOGGER.trace(lastGLError[versidx]);
}
versidx++;
} while (window == 0 && versidx < GL_VERSIONS.length);
// LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(12));
if (versidx== GL_VERSIONS.length && window == 0) {
LOGGER.error("Failed to find any valid GLFW profile. "+lastGLError[0]);
crashElegantly("Failed to find a valid GLFW profile.\nWe tried "+
Arrays.stream(GL_VERSIONS).map(p->p[0]+"."+p[1]).filter(o -> !skipVersions.contains(o))
.collect(Collector.of(()->new StringJoiner(", ").setEmptyValue("no versions"), StringJoiner::add, StringJoiner::merge, StringJoiner::toString))+
" but none of them worked.\n"+ Arrays.stream(lastGLError).filter(Objects::nonNull).collect(Collectors.joining("\n")));
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, version[0]);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, version[1]);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, GL_TRUE);
window = SDL_CreateWindow(vanillaWindowTitle, winWidth, winHeight,
SDL_WINDOW_OPENGL
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_HIGH_PIXEL_DENSITY
);
if (window == 0) {
var error = Objects.requireNonNullElse(SDLError.SDL_GetError(), "<no error>");
attempts.add(new Attempt(requestedVersion, error));
LOGGER.trace("Failed to create window using %d.%d: %s", version[0], version[1], error);
} else {
SDLVideo.SDL_SetWindowMinimumSize(window, 320, 240);
LOGGER.info("Created window using SDL video driver: {}", SDLVideo.SDL_GetCurrentVideoDriver());
break; // We got a valid window!
}
}
if (window == 0) {
LOGGER.error("Failed to find any valid GLFW profile.");
var message = new StringBuilder()
.append("Failed to find a valid GLFW profile.\n");
if (attempts.isEmpty()) {
message.append("All GL Versions were skipped");
} else {
message.append("We tried:");
for (var attempt : attempts)
message.append("\n\t").append(attempt.vesion).append(':').append(attempt.error);
}
crashElegantly(message.toString());
throw new IllegalStateException("Failed to create a GLFW window with any profile");
}
successfulWindow.set(true);
if (!windowFailFuture.cancel(true)) throw new IllegalStateException("We died but didn't somehow?");
var requestedVersion = GL_VERSIONS[versidx-1][0]+"."+GL_VERSIONS[versidx-1][1];
var maj = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);
var min = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);
var gotVersion = maj+"."+min;
LOGGER.info("Requested GL version "+requestedVersion+" got version "+gotVersion);
this.glVersion = gotVersion;
if (!windowFailFuture.cancel(true))
throw new IllegalStateException("We died but didn't somehow?");
try (var stack = org.lwjgl.system.MemoryStack.stackPush()) {
var majBuf = stack.mallocInt(1);
var minBuf = stack.mallocInt(1);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, majBuf);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minBuf);
this.glVersion = majBuf.get(0) + "." + minBuf.get(0);
}
LOGGER.info("Requested GL version " + requestedVersion + " got version " + this.glVersion);
this.window = window;
if (showHelpLog)
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG, false);
int[] x = new int[1];
int[] y = new int[1];
glfwGetMonitorPos(primaryMonitor, x, y);
int monitorX = x[0];
int monitorY = y[0];
// glfwSetWindowSizeLimits(window, 854, 480, GLFW_DONT_CARE, GLFW_DONT_CARE);
if (this.maximized) {
glfwMaximizeWindow(window);
}
if (this.maximized)
SDL_MaximizeWindow(window);
glfwGetWindowSize(window, x, y);
this.winWidth = x[0];
this.winHeight = y[0];
var size = getWindowSize(window);
this.onWindowResize(size.x, size.y);
// Setting the window position isn't supported on wayland, so check the error here
glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY);
handleLastGLFWError();
var monitor = getMonitorSize(primaryMonitor);
if (monitor != null)
SDL_SetWindowPosition(window, (monitor.width - size.x) / 2 + monitor.x, (monitor.height - size.y) / 2 + monitor.y);
// Attempt setting the icon
// int[] channels = new int[1];
// try (var glfwImgBuffer = GLFWImage.create(MemoryUtil.getAllocator().malloc(GLFWImage.SIZEOF), 1)) {
// final ByteBuffer imgBuffer;
// try (GLFWImage glfwImages = GLFWImage.malloc()) {
// imgBuffer = STBHelper.loadImageFromClasspath("forge_logo.png", 20000, x, y, channels);
// glfwImgBuffer.put(glfwImages.set(x[0], y[0], imgBuffer));
// glfwSetWindowIcon(window, glfwImgBuffer);
// STBImage.stbi_image_free(imgBuffer);
// }
// } catch (NullPointerException e) {
// System.err.println("Failed to load forge logo");
// }
// handleLastGLFWError((error, description) -> LOGGER.debug(String.format("Suppressing GLFW icon error: [0x%X]%s", error, description)));
pollEvents();
glfwSetFramebufferSizeCallback(window, this::fbResize);
glfwSetWindowPosCallback(window, this::winMove);
glfwSetWindowSizeCallback(window, this::winResize);
// Show the window
glfwShowWindow(window);
// Getting the window position isn't supported on wayland, so check the error here
glfwGetWindowPos(window, x, y);
handleLastGLFWError();
this.winX = x[0];
this.winY = y[0];
glfwGetFramebufferSize(window, x, y);
this.fbWidth = x[0];
this.fbHeight = y[0];
glfwPollEvents();
SDL_ShowWindow(window);
}
private static void handleLastGLFWError() {
handleLastGLFWError((error, description) -> {
if (error == GLFW_FEATURE_UNAVAILABLE) {
// suppress window pos errors for unsupported platforms (wayland)
LOGGER.debug(String.format("Suppressing GLFW error: [0x%X]%s", error, description));
return;
private record Point(int x, int y) {}
private record Rect(int x, int y, int width, int height) {}
private static @Nullable Rect getMonitorSize(int monitor) {
try (var bounds = SDL_Rect.malloc()) {
// Fetch the display bounds for the current index
if (SDL_GetDisplayBounds(monitor, bounds)) {
int x = bounds.x();
int y = bounds.y();
int width = bounds.w();
int height = bounds.h();
LOGGER.info("Display {} Bounds -> X: {}, Y: {}, Width: {}, Height: {}", monitor, x, y, width, height);
return new Rect(x, y, width, height);
} else {
LOGGER.warn("Could not get bounds for display: " + SDL_GetError());
return null;
}
throw new IllegalStateException(String.format("GLFW error: [0x%X]%s", error, description));
});
}
private void winResize(long window, int width, int height) {
if (window == this.window && width != 0 && height != 0) {
this.winWidth = width;
this.winHeight = height;
}
}
private void fbResize(long window, int width, int height) {
if (window == this.window && width != 0 && height != 0) {
this.fbWidth = width;
this.fbHeight = height;
}
}
private void winMove(long window, int x, int y) {
if (window == this.window) {
this.winX = x;
this.winY = y;
}
}
private static void handleLastGLFWError(BiConsumer<Integer, String> handler) {
try (MemoryStack memorystack = MemoryStack.stackPush()) {
PointerBuffer pointerbuffer = memorystack.mallocPointer(1);
int error = glfwGetError(pointerbuffer);
if (error != GLFW_NO_ERROR) {
long pDescription = pointerbuffer.get();
String description = pDescription == 0L ? "" : MemoryUtil.memUTF8(pDescription);
handler.accept(error, description);
}
private static Point getWindowSize(long window) {
try (var stack = MemoryStack.stackPush()) {
var width = stack.mallocInt(1);
var height = stack.mallocInt(1);
SDL_GetWindowSize(window, width, height);
return new Point(width.get(0), height.get(0));
}
}
@@ -553,7 +533,8 @@ public class DisplayWindow implements ImmediateWindowProvider {
*
* @return the Window we own.
*/
public long setupMinecraftWindow(final int width, final int height, final String title, final long monitorSupplier, final Supplier<Object> backend) {
@Override
public long setupMinecraftWindow(final int width, final int height, final String title, final Supplier<Object> backend) {
// wait for the window to actually be initialized
try {
this.initializationFuture.get(30, TimeUnit.SECONDS);
@@ -585,14 +566,10 @@ public class DisplayWindow implements ImmediateWindowProvider {
// we don't want the lock, just making sure it's back on the main thread
renderLock.release();
glfwMakeContextCurrent(window);
SDL_GL_MakeCurrent(window, sdlContext);
// Set the title to what the game wants
glfwSetWindowTitle(window, title);
glfwSwapInterval(0);
// Clean up our hooks
glfwSetFramebufferSizeCallback(window, null).free();
glfwSetWindowPosCallback(window, null).free();
glfwSetWindowSizeCallback(window, null).free();
SDL_SetWindowTitle(window, title);
SDL_GL_SetSwapInterval(0); // Disable vsync, let Minecraft control it
this.repaintTick = this::renderThreadFunc; // the repaint will continue to be called until the overlay takes over
this.windowTick = null; // this tells the render thread that the async ticker is done
return window;
@@ -658,7 +635,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
@Override
public void periodicTick() {
glfwPollEvents();
pollEvents();
repaintTick.run();
}
@@ -673,4 +650,67 @@ public class DisplayWindow implements ImmediateWindowProvider {
this.context.elementShader().close();
SimpleBufferBuilder.destroy();
}
private void bindGlContext() {
SDL_GL_MakeCurrent(window, sdlContext);
//if (!SDL_GL_MakeCurrent(window, sdlContext))
// throw new IllegalStateException("Failed to make OpenGL context current: " + SDL_GetError());
}
private void releaseGlContext() {
SDL_GL_MakeCurrent(window, 0);
}
private void pollEvents() {
try (var event = SDL_Event.malloc()) {
while (SDL_PollEvent(event)) {
switch (event.type()) {
case SDL_EVENT_WINDOW_MOVED:
this.onWindowMove(event.window().data1(), event.window().data2());
break;
case SDL_EVENT_WINDOW_RESIZED:
this.onWindowResize(event.window().data1(), event.window().data2());
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
this.onFrameBufferResize(event.window().data1(), event.window().data2());
break;
/*
We were told to quit for some reason, find a way to abort?
case SDL_EVENT_QUIT:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case SDL_EVENT_TERMINATING:
Window was minimized or restored, maybe stop rendering?
case SDL_EVENT_WINDOW_MINIMIZED:
case SDL_EVENT_WINDOW_MAXIMIZED:
case SDL_EVENT_WINDOW_RESTORED:
*/
}
}
}
}
private void onWindowResize(int width, int height) {
if (width == 0 || height == 0)
return;
this.winWidth = width;
this.winHeight = height;
}
private void onFrameBufferResize(int width, int height) {
if (width == 0 || height == 0)
return;
this.fbWidth = width;
this.fbHeight = height;
}
private void onWindowMove(int x, int y) {
this.winX = x;
this.winY = y;
}
}
@@ -11,8 +11,6 @@ import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static org.lwjgl.opengl.GL32C.*;
public class RenderElement {
@@ -39,11 +37,6 @@ public class RenderElement {
void accept(SimpleBufferBuilder bb, DisplayContext context, int[] size, int frame);
}
/** @deprecated Use {@link Renderer} directly. This was always eagerly resolved, making it an unnecessary wrapper. */
@Deprecated(since = "1.21.5", forRemoval = true)
@FunctionalInterface
interface Initializer extends Supplier<Renderer> {}
@FunctionalInterface
interface TextGenerator {
void accept(SimpleBufferBuilder bb, SimpleFont fh, DisplayContext ctx);
@@ -59,13 +52,6 @@ public class RenderElement {
}
}
/** @deprecated Use {@link RenderElement#RenderElement(Renderer)} instead */
@Deprecated(since = "1.21.5", forRemoval = true)
public RenderElement(final Initializer rendererInitializer) {
this.bb = new SimpleBufferBuilder(1);
this.renderer = rendererInitializer.get();
}
public RenderElement(Renderer renderer) {
this.bb = new SimpleBufferBuilder(1);
this.renderer = renderer;
+18
View File
@@ -0,0 +1,18 @@
plugins {
id 'net.minecraftforge.forge.build.convention'
id 'jvm-test-suite'
}
gradleutils.displayName = 'FMLLoader-Tests'
description = 'JUnit tests for FMLLoader, not meant for distribution'
dependencies {
testImplementation(project(':fmlloader'))
testImplementation(libs.guava)
testImplementation(libs.junit.api)
testRuntimeOnly(libs.bundles.junit.runtime)
}
tasks.named('test') {
useJUnitPlatform()
}
@@ -0,0 +1,172 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading.test;
import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import net.minecraftforge.fml.loading.toposort.CyclePresentException;
import net.minecraftforge.fml.loading.toposort.StronglyConnectedComponentDetector;
import net.minecraftforge.fml.loading.toposort.TopologicalSort;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for {@link TopologicalSort} and {@link StronglyConnectedComponentDetector}
*/
class TopologicalSortTest {
@Test
void resultProperlySorted() {
MutableGraph<String> graph = GraphBuilder.directed().build();
graph.putEdge("a", "b");
graph.putEdge("a", "c");
graph.putEdge("b", "c");
var sorted = TopologicalSort.topologicalSort(graph, Comparator.naturalOrder());
assertEquals(List.of("a", "b", "c"), sorted);
}
@Test
void throwsOnCycle() {
MutableGraph<String> graph = GraphBuilder.directed().build();
graph.putEdge("a", "b");
graph.putEdge("b", "a");
var exception = Assertions.assertThrows(CyclePresentException.class, () -> TopologicalSort.topologicalSort(graph, Comparator.naturalOrder()));
assertEquals(Set.of(
Set.of("a", "b")
), exception.getCycles());
}
@Test
void simpleCycle() {
MutableGraph<String> graph = GraphBuilder.directed().build();
// a -> b -> c -> a
graph.putEdge("a", "b");
graph.putEdge("b", "c");
graph.putEdge("c", "a");
var cycles = cyclesOf(graph);
assertEquals(Set.of(
Set.of("a", "b", "c")
), cycles);
}
@Test
void noCycle() {
MutableGraph<String> graph = GraphBuilder.directed().build();
graph.putEdge("a", "b");
graph.putEdge("a", "c");
graph.putEdge("b", "d");
graph.putEdge("c", "d");
graph.putEdge("d", "e");
var cycles = cyclesOf(graph);
assertTrue(cycles.isEmpty());
}
/**
* Regression test: the previous implementation lowered the lowlink of a node from every already-visited
* successor, including successors whose component was already completed. That merged nodes into the wrong
* component, so a member of the real cycle ({@code c}) was missing from the reported cycle.
*/
@Test
void reportsAllMembersOfCycle() {
MutableGraph<String> graph = GraphBuilder.directed().build();
graph.addNode("a");
graph.addNode("b");
graph.addNode("c");
graph.putEdge("a", "b");
graph.putEdge("a", "c");
graph.putEdge("b", "a");
graph.putEdge("c", "b");
var cycles = cyclesOf(graph);
assertEquals(Set.of(
Set.of("a", "b", "c")
), cycles);
}
/**
* Regression test: the previous implementation invented a cycle out of {@code c -> d -> a}, which is a plain
* path, because it compared against the DFS depth instead of a discovery counter and ignored whether a
* successor was still on the stack.
*/
@Test
void doesNotInventCycles() {
MutableGraph<String> graph = GraphBuilder.directed().build();
graph.addNode("a");
graph.addNode("b");
graph.addNode("c");
graph.addNode("d");
graph.putEdge("a", "b");
graph.putEdge("b", "a");
graph.putEdge("c", "d");
graph.putEdge("d", "a");
var cycles = cyclesOf(graph);
assertEquals(Set.of(
Set.of("a", "b")
), cycles);
}
/**
* Reproduces the shape of the mod graphs that reported bogus cycles in the wild: one real three-node cycle,
* with nodes that merely depend on members of the cycle and a node on a plain path into it.
*
* <p>Every reported component must be the real cycle itself; nodes such as {@code dependent_a} only depend on
* the cycle and must not be reported.
*
* @see <a href="https://github.com/MinecraftForge/MinecraftForge/pull/10924">related GitHub issue</a>
*/
@Test
void reportsOnlyRealCycles() {
MutableGraph<String> graph = GraphBuilder.directed().build();
// Mirrors the insertion order of ModSorter: every mod is added as a node first, edges are added afterwards.
for (String mod : List.of("upstream", "cycle_a", "cycle_b", "dependent_a", "cycle_c", "dependent_b")) {
graph.addNode(mod);
}
// a -> b -> a
graph.putEdge("cycle_a", "cycle_b");
graph.putEdge("cycle_b", "cycle_a");
// a -> c -> b -> a
graph.putEdge("cycle_a", "cycle_c");
graph.putEdge("cycle_c", "cycle_b");
// dA -> b -> a - Valid
graph.putEdge("dependent_a", "cycle_b");
// dB -> b -> a - Valid
graph.putEdge("dependent_b", "cycle_b");
// u -> a -> b -> a - Not valid, but u is not part of the cycle so shouldn't be reported
graph.putEdge("upstream", "cycle_a");
var cycles = cyclesOf(graph);
assertEquals(Set.of(
Set.of("cycle_a", "cycle_b", "cycle_c")
), cycles);
var exception = Assertions.assertThrows(CyclePresentException.class, () -> TopologicalSort.topologicalSort(graph, Comparator.naturalOrder()));
assertEquals(Set.of(
Set.of("cycle_a", "cycle_b", "cycle_c")
), exception.getCycles());
}
private static Set<Set<String>> cyclesOf(Graph<String> graph) {
var detector = new StronglyConnectedComponentDetector<>(graph);
var connected = detector.getComponents();
var cycles = new HashSet<>(connected);
cycles.removeIf(component -> component.size() < 2);
return cycles;
}
}
+16 -120
View File
@@ -1,44 +1,15 @@
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import net.minecraftforge.forgedev.legacy.tasks.JarJarMetadataOptions
import org.gradle.api.plugins.jvm.JvmTestSuite
import javax.inject.Inject
plugins {
id 'java-library'
id 'jvm-test-suite'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
alias libs.plugins.apt
id 'net.minecraftforge.forge.build.convention'
alias libs.plugins.apt
}
gradleutils.displayName = 'FMLLoader'
final vendor = 'Forge Development LLC'
description = 'Modifications to Minecraft to enable mod developers.'
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
description = 'Mod Locator and manager for Minecraft Forge.'
dependencies {
compileOnly libs.jetbrains.annotations
// TODO [FMLLoader] Figure out a better way to lock transitive dependencies to the explicit versions shipped by the isntaller
// TODO [FMLLoader] Figure out a better way to lock transitive dependencies to the explicit versions shipped by the installer
api(libs.bundles.asm ){ transitive = false } // Needed by all the black magic
api(libs.forgespi ){ transitive = false }
api(libs.mergetool.api ){ transitive = false }
@@ -58,7 +29,6 @@ dependencies {
implementation(libs.securemodules ){ transitive = false }
implementation(libs.accesstransformers ){ transitive = false }
implementation(libs.terminalconsoleappender){ transitive = false }
implementation(libs.jimfs ){ transitive = false }
implementation(libs.roimfs ){ transitive = false }
// Needed because we have a custom log4j plugin, and they removed package scanning and require a data file to be generated
@@ -66,93 +36,19 @@ dependencies {
annotationProcessor(libs.bundles.log4j){ transitive = false }
}
testing.suites.named('test', JvmTestSuite) {
dependencies {
compileOnly libs.jetbrains.annotations
runtimeOnly libs.bootstrap
}
}
convention.manifest('net/minecraftforge/fml/loading/', [
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
'Forge-Module-Layer' : 'boot'
])
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
'Forge-Module-Layer' : 'boot'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Vendor' : vendor,
'Implementation-Version': forgeVersion
], 'net/minecraftforge/fml/loading/')
}
}
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
// A simple task to write JSON to a file. Using Task#doLast to output files is no longer supported.
// This code may seem verbose, but it's better this way. If more projects need this, we can move it to buildSrc.
@CompileStatic
@PackageScope abstract class WriteForgeVersionJson extends DefaultTask {
abstract @Input Property<String> getForgeVersion()
abstract @Input Property<String> getMinecraftVersion()
abstract @Input Property<String> getMcpVersion()
abstract @OutputFile RegularFileProperty getOutputFile()
@Inject
WriteForgeVersionJson() {}
@TaskAction
@CompileDynamic
void exec() {
var json = new groovy.json.JsonBuilder()
json {
forge this.forgeVersion.get()
mc this.minecraftVersion.get()
mcp this.mcpVersion.get()
}
this.outputFile.asFile.get().text = json.toPrettyString()
}
}
tasks.register('writeForgeVersionJson', WriteForgeVersionJson) {
forgeVersion = project.forgeVersion
minecraftVersion = project.minecraftVersion
mcpVersion = project.mcpVersion
convention.publish()
final forgeJson = convention.forgeVersionJson {
outputFile = file('src/main/resources/forge_version.json')
}
tasks.register('jarJarOptionsJson', JarJarMetadataOptions) {
metadataFile = project.file('src/main/resources/jarjar_options.json')
final jarJarJson = tasks.register('jarJarOptionsJson', JarJarMetadataOptions) {
metadataFile = file('src/main/resources/jarjar_options.json')
// This is resolved too early by cpw.mods.modlauncher.TransformationServicesHandler.discoverServices(DiscoveryData) But eventually...
//add(libs.mixin, 'org/spongepowered/asm/mixin/Mixin.class')
add(libs.mixinextras.forge) {
@@ -168,11 +64,11 @@ tasks.register('jarJarOptionsJson', JarJarMetadataOptions) {
tasks.named('generateResources') {
dependsOn(
tasks.named('eclipseJdt'),
tasks.named('eclipseJdtApt'),
tasks.named('eclipseFactorypath'),
tasks.named('writeForgeVersionJson', WriteForgeVersionJson),
tasks.named('jarJarOptionsJson')
tasks.named('eclipseJdt'),
tasks.named('eclipseJdtApt'),
tasks.named('eclipseFactorypath'),
forgeJson,
jarJarJson
)
}
@@ -136,7 +136,7 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
classNode.methods.stream().filter(m -> m.name.equals("<init>")).forEach(m -> sb.append(" : ").append(m.desc).append("\n"));
LOGGER.error(LogUtils.FATAL_MARKER, sb.toString());
}
throw new IllegalStateException("Enum has create method with no matching constructor: " + desc);
throw new IllegalStateException("Enum has create method with no matching constructor: " + classType.getClassName() + " " + desc);
}
if (values == null)
@@ -102,8 +102,8 @@ public class ImmediateWindowHandler {
return "default";
}
public static long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend) {
return provider.setupMinecraftWindow(width, height, title, monitor, backend);
public static long setupMinecraftWindow(final int width, final int height, final String title, final Supplier<Object> backend) {
return provider.setupMinecraftWindow(width, height, title, backend);
}
public static boolean positionWindow(Optional<Object> monitor,IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) {
@@ -155,9 +155,9 @@ public class ImmediateWindowHandler {
}
@Override
public long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend) {
public long setupMinecraftWindow(final int width, final int height, final String title, final Supplier<Object> backend) {
try {
var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, monitor, backend);
var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, backend);
return longsupplier.getAsLong();
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
@@ -86,10 +86,9 @@ public interface ImmediateWindowProvider {
* @param width This is the width of the window Mojang expects
* @param height This is the height of the Window Mojang expects.
* @param title This is the title for the window.
* @param monitor This is the monitor it should appear on.
* @return The window id
*/
long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend);
long setupMinecraftWindow(final int width, final int height, final String title, final Supplier<Object> backend);
/**
* This is called after window handoff to allow us to tell Mojang about our window's position. This might give a
@@ -5,39 +5,16 @@
package net.minecraftforge.fml.loading.targets;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.ApiStatus;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
import com.electronwill.nightconfig.core.io.WritingMode;
import com.electronwill.nightconfig.toml.TomlParser;
import com.electronwill.nightconfig.toml.TomlWriter;
import com.google.common.jimfs.Jimfs;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.moddiscovery.AbstractModProvider;
import net.minecraftforge.forgespi.locating.IModLocator;
@ApiStatus.Internal
public final class ForgeDevLocator extends AbstractModProvider implements IModLocator {
private static final String PACK_META = "pack.mcmeta";
@Override
public String name() {
return "forge_dev_locator";
@@ -71,228 +48,6 @@ public final class ForgeDevLocator extends AbstractModProvider implements IModLo
}
var ret = new ArrayList<Path>();
ret.add(forge);
var isTest = Boolean.getBoolean("forgedev.enableTestMods");
if (!isTest)
return ret;
var test = ForgeDevLaunchHandler.getPathFromResource("net/minecraftforge/test/BaseTestMod.class", ClassLoader.getSystemClassLoader());
// Explode our test mods into a framework that treats them all as separate jars.
var tests = explodeTestMods(test);
ret.addAll(tests);
return ret;
}
private static List<Path> explodeTestMods(Path path) {
var mod = new ArrayList<Path>();
var memory = Jimfs.newFileSystem();
// First lets find all class files, that have the @Mod annotation and map packages to modids.
var packages = findTestModPackages(path);
for (var entry : packages.entrySet()) {
var pkg = entry.getKey() + '/';
var modids = entry.getValue();
// Find resource directories for every mod in this group
var paths = new LinkedHashSet<Path>();
for (var modid : modids.stream().sorted().toList()) {
var rsc = path.resolve(modid);
if (Files.exists(rsc))
paths.add(rsc);
}
var root = memory.getPath(modids.iterator().next()); // use the first modid as our root
buildModsToml(paths, modids, root, pkg.startsWith("net/minecraftforge/debug/client/"));
buildPackMeta(paths, root);
moveModuleInfo(paths, root);
if (Files.exists(root))
paths.add(root);
// We want just the class files from the root of the input paths. So make a new union with a filter.
var classes = UnionHelper.newFileSystem(
(name, _) -> {
if (name.endsWith("/")) {
if (name.startsWith("/"))
name = name.substring(1);
return pkg.startsWith(name) || name.startsWith(pkg);
}
return name.endsWith(".class") && name.startsWith(pkg);
}, new Path[] { path }
);
paths.addFirst(classes.getRootDirectories().iterator().next());
// Union of unions, Yay!
var union = UnionHelper.newFileSystem(null, paths.stream().toArray(Path[]::new));
mod.add(union.getRootDirectories().iterator().next());
}
return mod;
}
// Find all the @Mods so we can generate tomls, and so we can pick packages. Right now it doesn't allow mods in parent directories,
// I could make it merge all the way up, but I think this would be fine.
private static Map<String, Set<String>> findTestModPackages(Path path) {
var mods = new HashMap<String, Set<String>>();
try (var files = Files.walk(path)) {
var classes = files
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".class"))
.toList();
for (var cls : classes) {
try (var is = Files.newInputStream(cls)) {
var reader = new ClassReader(is);
reader.accept(new ClassVisitor(Opcodes.ASM9) {
private String clsName;
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.clsName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String name, boolean runtime) {
if (!"Lnet/minecraftforge/fml/common/Mod;".equals(name))
return super.visitAnnotation(name, runtime);
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public void visit(String key, Object value) {
if ("value".equals(key)) {
int idx = clsName.lastIndexOf('/');
var pkg = clsName.substring(0, idx);
mods.computeIfAbsent(pkg, _ -> new HashSet<>()).add((String)value);
idx = pkg.lastIndexOf('/');
while (idx != -1) {
pkg = pkg.substring(0, idx);
idx = pkg.lastIndexOf('/');
if (mods.containsKey(pkg))
throw new IllegalStateException("Invalid ForgeDev test mod layout, conflicting packages: " + pkg + " for " + clsName);
}
}
}
};
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
}
}
} catch (IOException e) {
sneak(e);
}
return mods;
}
// Builds or update the mods.toml file for all @Mods in this package
private static void buildModsToml(Set<Path> resources, Set<String> modids, Path root, boolean clientOnly) {
var toml = resources.stream()
.map(p -> p.resolve(MODS_TOML))
.filter(Files::exists)
.findFirst()
.orElse(null);
Config cfg = null;
if (toml != null)
cfg = new TomlParser().parse(toml, FileNotFoundAction.READ_NOTHING);
else
cfg = Config.inMemory();
boolean modified = false;
var defaults = Map.of(
"modLoader", "javafml",
"loaderVersion", "[0,)",
"license", "Doesnt fucking matter",
"clientSideOnly", clientOnly
);
for (var key : defaults.keySet()) {
if (!cfg.contains(key)) {
cfg.set(key, defaults.get(key));
modified = true;
}
}
var modlist = new ArrayList<Config>();
if (cfg.contains("mods")) {
@SuppressWarnings("unchecked")
var existing = ((List<Config>)cfg.get("mods"));
modlist.addAll(existing);
}
for (var modid : modids) {
if (modlist.stream().noneMatch(c -> modid.equals(c.get("modId")))) {
modified = true;
var tmp = Config.inMemory();
tmp.set("modId", modid);
modlist.add(tmp);
}
}
cfg.set("mods", modlist);
if (modified) {
var target = root.resolve(MODS_TOML);
try {
Files.createDirectories(target.getParent());
new TomlWriter().write(cfg, target, WritingMode.REPLACE);
} catch (IOException e) {
throw new IllegalStateException("Failed to create in memory toml: " + target, e);
}
}
}
// This is optional, it just hides a warning screen when starting up. I should probably remove this once I restructure how data gen for test mods work and make it generate there.
private static void buildPackMeta(Set<Path> paths, Path root) {
var existing = paths.stream()
.map(p -> p.resolve(PACK_META))
.filter(Files::exists)
.findFirst()
.orElse(null);
if (existing != null)
return;
var target = root.resolve(PACK_META);
try {
Files.createDirectories(target.getParent());
Files.writeString(target,
"""
{
"pack": {
"description": "doesn't matter",
"pack_format": 18
}
}
""", StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
} catch (IOException e) {
throw new IllegalStateException("Failed to make in memory pack.mcmeta: " + target, e);
}
}
private static void moveModuleInfo(Set<Path> paths, Path root) {
var existing = paths.stream()
.map(p -> p.resolve("module-info.dat"))
.filter(Files::exists)
.findFirst()
.orElse(null);
if (existing == null)
return;
var target = root.resolve("module-info.class");
try {
Files.createDirectories(target.getParent());
Files.copy(existing, target);
} catch (IOException e) {
throw new IllegalStateException("Failed to copy module-info.dat to memory: " + target, e);
}
}
@SuppressWarnings("unchecked")
private static <E extends Throwable, R> R sneak(Throwable e) throws E {
throw (E)e;
}
}
@@ -22,15 +22,18 @@ import java.util.Set;
*
* <p>This detector evaluates the graph lazily and won't reflect the modifications in the
* graph after initial evaluation.
*
* <p> Implementation based on <a href="https://en.wikipedia.org/w/index.php?title=Tarjan%27s_strongly_connected_components_algorithm&oldid=1384955226">Wikipedia Psudocode</a>
*/
public class StronglyConnectedComponentDetector<T> {
private final Graph<T> graph;
private final Map<T, Integer> ids = new HashMap<>();
private T[] elements;
private int[] dfn;
private int[] low;
private int[] indexes;
private int[] lowlinks;
private int[] stack;
private int top;
private int index;
private BitSet onStack;
private Set<Set<T>> components;
@@ -58,48 +61,43 @@ public class StronglyConnectedComponentDetector<T> {
}
final int n = nodes.size();
dfn = new int[n];
low = new int[n];
indexes = new int[n];
lowlinks = new int[n];
stack = new int[n];
onStack = new BitSet(n);
top = -1;
index = 1;
for (int i = 0; i < n; i++) {
if (dfn[i] == 0) {
dfs(i, 1);
}
if (indexes[i] == 0)
dfs(i);
}
}
private void dfs(int now, int depth) {
dfn[now] = depth;
low[now] = depth;
top++;
stack[top] = now;
private void dfs(int now) {
indexes[now] = lowlinks[now] = index++;
stack[++top] = now;
onStack.set(now);
for (T each : graph.successors(elements[now])) {
int to = ids.get(each);
if (dfn[to] != 0) {
if (low[now] > dfn[to]) {
low[now] = dfn[to];
}
} else {
dfs(to, depth + 1);
if (low[now] > low[to]) {
low[now] = low[to];
}
if (indexes[to] == 0) { // 0 is unvisited
dfs(to);
if (lowlinks[now] > lowlinks[to])
lowlinks[now] = lowlinks[to];
} else if (onStack.get(to)) {
if (lowlinks[now] > indexes[to])
lowlinks[now] = indexes[to];
}
}
if (dfn[now] == low[now]) {
if (indexes[now] == lowlinks[now]) {
Set<T> component = new HashSet<>();
while (top >= 0) {
final int t = stack[top];
component.add(elements[t]);
onStack.clear(t);
top--;
if (t == now) {
final int w = stack[top--];
onStack.clear(w);
component.add(elements[w]);
if (w == now)
break;
}
}
components.add(component);
}
+19
View File
@@ -0,0 +1,19 @@
plugins {
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'Forge-Examiner'
description = 'Forge Mod Locator for multiple mods merged together into one code source'
dependencies {
implementation project(':fmlloader')
implementation(libs.jimfs){ transitive = false }
implementation(libs.bootstrap.api)
}
convention.manifest('net/minecraftforge/examiner/', [
'Automatic-Module-Name': 'net.minecraftforge.examiner',
'Forge-Module-Layer' : 'boot'
])
convention.publish()
@@ -0,0 +1,38 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.examiner;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import net.minecraftforge.bootstrap.api.BootstrapClasspathModifier;
public class BootstrapFilter implements BootstrapClasspathModifier {
@Override
public String name() {
return "examiner";
}
@Override
public boolean process(List<Path[]> classpath) {
var modified = false;
for (var itr = classpath.iterator(); itr.hasNext(); ) {
for (var path : itr.next()) {
final var target = path.resolve(Locator.MARKER);
if (Files.exists(target)) {
itr.remove();
modified = true;
break;
}
}
}
return modified;
}
}
@@ -0,0 +1,338 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.examiner;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
import com.electronwill.nightconfig.core.io.WritingMode;
import com.electronwill.nightconfig.toml.TomlParser;
import com.electronwill.nightconfig.toml.TomlWriter;
import com.google.common.jimfs.Jimfs;
import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.loading.LogMarkers;
import net.minecraftforge.fml.loading.moddiscovery.AbstractModProvider;
import net.minecraftforge.forgespi.locating.IModLocator;
public class Locator extends AbstractModProvider implements IModLocator {
private static final Logger LOGGER = LogUtils.getLogger();
static final String MARKER = ".examiner";
private static final String PACK_META = "pack.mcmeta";
private static final String PROJECT_INFO = ".project_info.properties";
@Override
public String name() {
return "examiner";
}
@Override
public List<ModFileOrException> scanMods() {
var mods = getMods();
var ret = new ArrayList<ModFileOrException>();
for (var path : mods) {
var mod = createMod(path);
if (mod != null)
ret.add(mod);
}
return ret;
}
private Collection<Path> getMods() {
var ret = new HashSet<Path>();
var cl = ClassLoader.getSystemClassLoader();
var markers = getUrls(cl, MARKER);
markers.addAll(getUrls(getClass().getClassLoader(), MARKER));
for (var url : markers) {
var path = getPath(url, MARKER);
var mods = explodeTestMods(path);
for (var mod : mods)
ret.add(mod);
}
return ret;
}
private static Set<URL> getUrls(ClassLoader cl, String resource) {
try {
var lst = Collections.list(cl.getResources(resource));
if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) {
lst.sort(Comparator.comparing(URL::toString));
LOGGER.debug(LogMarkers.SCAN, "Scanning Classloader: {} for {}", cl, resource);
for (var url : lst)
LOGGER.debug(LogMarkers.SCAN, "\t{}", url);
}
return new LinkedHashSet<URL>(lst);
} catch (IOException e) {
LOGGER.warn(LogMarkers.SCAN, "Error finding resources for: " + resource, e);
return Set.of();
}
}
private static Path getPath(URL url, String resource) {
var str = url.toString();
int len = resource.length();
if ("jar".equalsIgnoreCase(url.getProtocol())) {
str = url.getFile();
len += 2;
}
str = str.substring(0, str.length() - len);
return Path.of(URI.create(str));
}
private static List<Path> explodeTestMods(Path path) {
var mod = new ArrayList<Path>();
var memory = Jimfs.newFileSystem();
var info = path.resolve(PROJECT_INFO);
if (!Files.exists(info))
info = null;
// First lets find all class files, that have the @Mod annotation and map packages to modids.
var packages = findTestModPackages(path);
if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) {
var ordered = packages.entrySet().stream().sorted((e1, e2) -> e1.getKey().compareTo(e2.toString())).toList();
for (var entry : ordered)
LOGGER.debug(LogMarkers.SCAN, "Examiner located {} with mods {}", entry.getKey(), entry.getValue().stream().sorted().toList());
}
for (var entry : packages.entrySet()) {
var pkg = entry.getKey() + '/';
var modids = entry.getValue();
// Find resource directories for every mod in this group
var paths = new LinkedHashSet<Path>();
for (var modid : modids.stream().sorted().toList()) {
var rsc = path.resolve(modid);
if (Files.exists(rsc))
paths.add(rsc);
}
var root = memory.getPath(modids.iterator().next()); // use the first modid as our root
buildModsToml(paths, modids, root, pkg.startsWith("net/minecraftforge/debug/client/"));
buildPackMeta(paths, root);
moveModuleInfo(paths, root);
copyFile(info, root.resolve(PROJECT_INFO));
if (Files.exists(root))
paths.add(root);
// We want just the class files from the root of the input paths. So make a new union with a filter.
var classes = UnionHelper.newFileSystem(
(name, _) -> {
if (name.endsWith("/")) {
if (name.startsWith("/"))
name = name.substring(1);
return pkg.startsWith(name) || name.startsWith(pkg);
}
return name.endsWith(".class") && name.startsWith(pkg);
}, new Path[] { path }
);
paths.addFirst(classes.getRootDirectories().iterator().next());
// Union of unions, Yay!
var union = UnionHelper.newFileSystem(null, paths.stream().toArray(Path[]::new));
mod.add(union.getRootDirectories().iterator().next());
}
return mod;
}
// Find all the @Mods so we can generate tomls, and so we can pick packages. Right now it doesn't allow mods in parent directories,
// I could make it merge all the way up, but I think this would be fine.
private static Map<String, Set<String>> findTestModPackages(Path path) {
var mods = new HashMap<String, Set<String>>();
try (var files = Files.walk(path)) {
var classes = files
.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".class"))
.toList();
for (var cls : classes) {
try (var is = Files.newInputStream(cls)) {
var reader = new ClassReader(is);
reader.accept(new ClassVisitor(Opcodes.ASM9) {
private String clsName;
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.clsName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String name, boolean runtime) {
if (!"Lnet/minecraftforge/fml/common/Mod;".equals(name))
return super.visitAnnotation(name, runtime);
return new AnnotationVisitor(Opcodes.ASM9) {
@Override
public void visit(String key, Object value) {
if ("value".equals(key)) {
int idx = clsName.lastIndexOf('/');
var pkg = clsName.substring(0, idx);
mods.computeIfAbsent(pkg, _ -> new HashSet<>()).add((String)value);
idx = pkg.lastIndexOf('/');
while (idx != -1) {
pkg = pkg.substring(0, idx);
idx = pkg.lastIndexOf('/');
if (mods.containsKey(pkg))
throw new IllegalStateException("Invalid ForgeDev test mod layout, conflicting packages: " + pkg + " for " + clsName);
}
}
}
};
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
}
}
} catch (IOException e) {
sneak(e);
}
return mods;
}
// Builds or update the mods.toml file for all @Mods in this package
private static void buildModsToml(Set<Path> resources, Set<String> modids, Path root, boolean clientOnly) {
var toml = resources.stream()
.map(p -> p.resolve(MODS_TOML))
.filter(Files::exists)
.findFirst()
.orElse(null);
Config cfg = null;
if (toml != null)
cfg = new TomlParser().parse(toml, FileNotFoundAction.READ_NOTHING);
else
cfg = Config.inMemory();
boolean modified = false;
var defaults = Map.of(
"modLoader", "javafml",
"loaderVersion", "[0,)",
"license", "Doesnt fucking matter",
"clientSideOnly", clientOnly
);
for (var key : defaults.keySet()) {
if (!cfg.contains(key)) {
cfg.set(key, defaults.get(key));
modified = true;
}
}
var modlist = new ArrayList<Config>();
if (cfg.contains("mods")) {
@SuppressWarnings("unchecked")
var existing = ((List<Config>)cfg.get("mods"));
modlist.addAll(existing);
}
for (var modid : modids) {
if (modlist.stream().noneMatch(c -> modid.equals(c.get("modId")))) {
modified = true;
var tmp = Config.inMemory();
tmp.set("modId", modid);
modlist.add(tmp);
}
}
cfg.set("mods", modlist);
if (modified) {
var target = root.resolve(MODS_TOML);
try {
Files.createDirectories(target.getParent());
new TomlWriter().write(cfg, target, WritingMode.REPLACE);
} catch (IOException e) {
throw new IllegalStateException("Failed to create in memory toml: " + target, e);
}
}
}
// This is optional, it just hides a warning screen when starting up. I should probably remove this once I restructure how data gen for test mods work and make it generate there.
private static void buildPackMeta(Set<Path> paths, Path root) {
var existing = paths.stream()
.map(p -> p.resolve(PACK_META))
.filter(Files::exists)
.findFirst()
.orElse(null);
if (existing != null)
return;
var target = root.resolve(PACK_META);
try {
Files.createDirectories(target.getParent());
Files.writeString(target,
"""
{
"pack": {
"description": "doesn't matter",
"pack_format": 18
}
}
""", StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
} catch (IOException e) {
throw new IllegalStateException("Failed to make in memory pack.mcmeta: " + target, e);
}
}
private static void moveModuleInfo(Set<Path> paths, Path root) {
var existing = paths.stream()
.map(p -> p.resolve("module-info.dat"))
.filter(Files::exists)
.findFirst()
.orElse(null);
var target = root.resolve("module-info.class");
copyFile(existing, target);
}
private static void copyFile(@Nullable Path existing, Path target) {
if (existing == null)
return;
try {
Files.createDirectories(target.getParent());
Files.copy(existing, target);
} catch (IOException e) {
throw new IllegalStateException("Failed to copy " + existing.getFileName() + " to memory: " + target, e);
}
}
@SuppressWarnings("unchecked")
private static <E extends Throwable, R> R sneak(Throwable e) throws E {
throw (E)e;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.examiner;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.function.BiPredicate;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
final class UnionHelper {
private static final FileSystemProvider UFSP = getUnionFileSystemProvider();
private static FileSystemProvider getUnionFileSystemProvider() {
for (var provider : FileSystemProvider.installedProviders()) {
if ("union".equals(provider.getScheme()))
return provider;
}
throw new IllegalStateException("Missing UnionFileSystemProvider");
};
static FileSystem newFileSystem(BiPredicate<String, String> filter, Path[] paths) {
try {
if (paths == null || paths.length == 0)
throw new IllegalArgumentException("Must contain atleast one path");
if (filter == null && paths.length == 1)
return FileSystems.newFileSystem(paths[0]);
var map = new HashMap<String, Object>();
if (filter != null)
map.put("filter", filter);
var lst = new ArrayList<>(Arrays.asList(paths));
var base = lst.remove(0);
map.put("additional", lst);
return UFSP.newFileSystem(base, map);
} catch (IOException e) {
return sneak(e);
}
}
@SuppressWarnings("unchecked")
private static <E extends Throwable, R> R sneak(Throwable e) throws E {
throw (E)e;
}
}
@@ -0,0 +1 @@
net.minecraftforge.examiner.BootstrapFilter
@@ -0,0 +1 @@
net.minecraftforge.examiner.Locator
+68
View File
@@ -0,0 +1,68 @@
plugins {
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'Forge-Tests'
description = 'Mods used for testing Forge features'
license {
exclude 'com/example/'
}
convention.basicRuns('mdk_datagen', file('src/main/generated')) {
flat = true
}
final mdkGradleWrapper = tasks.register('mdkGradleWrapper', Wrapper) {
gradleVersion = libs.versions.gradle.get()
scriptFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradlew")
jarFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradle/wrapper/gradle-wrapper.jar")
}
final mdkZip = tasks.register('mdkZip', Zip) {
dependsOn mdkGradleWrapper
archiveBaseName = project.name
archiveClassifier = 'mdk'
archiveVersion = project.version
destinationDirectory = file('build/libs')
from(rootProject.file('LICENSE.txt'))
from(changelog.task.flatMap(task -> task.outputFile))
from mdkGradleWrapper.map(Wrapper.&getScriptFile)
from mdkGradleWrapper.map(Wrapper.&getBatchScript)
into('gradle/wrapper/') {
from mdkGradleWrapper.map(Wrapper.&getJarFile)
from mdkGradleWrapper.map(Wrapper.&getPropertiesFile)
}
from(file('template/')){
file('template/gitignore.txt').eachLine {
if (!it.trim().isEmpty() && !it.trim().startsWith('#'))
exclude it
}
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [
FORGE_VERSION: versions.forge,
FORGE_GROUP: project.group,
FORGE_NAME: project.name,
MC_VERSION: versions.minecraft,
FORGE_SPEC_VERSION: versions.forgeSpec,
MC_NEXT_VERSION: versions.minecraftNext,
EVENTBUS_VERSION: libs.versions.eventbus.get()
])
rename 'gitignore\\.txt', '.gitignore'
rename 'gitattributes\\.txt', '.gitattributes'
}
from(file('src/main/java/com/example/examplemod/')) {
into('src/main/java/com/example/examplemod/')
}
from(file('src/main/generated/')) {
into('src/main/resources/')
exclude '**/.cache/'
}
}
forgedev.provideFile('mdkZip', mdkZip.flatMap { it.archiveFile } )
+7
View File
@@ -0,0 +1,7 @@
{
"pack": {
"description": "${mod_id} resources",
"max_format": 121,
"min_format": 121
}
}
@@ -0,0 +1,13 @@
modLoader="javafml"
loaderVersion="[0,)"
license="LGPL_v2"
[[mods]]
modId="examplemod"
version="1.0.0"
displayName="Example Mod DataGen"
description="Example Mod DataGen"
[[mods]]
modId="mdk_datagen"
version="1.0.0"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+25
View File
@@ -0,0 +1,25 @@
plugins {
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'Forge-Tests'
description = 'Mods used for testing Forge features'
license {
exclude 'com/example/'
}
convention.manifest('net/minecraftforge/forge/test/', [
'Automatic-Module-Name': 'net.minecraftforge.forge.test'
])
convention.manifest('net/minecraftforge/forge/debug/')
convention.publish()
convention.basicRuns('.+', file('src/main/generated')) {
flat = false
}
dependencies {
api project(':forge-examiner')
}
@@ -0,0 +1,3 @@
{
"type": "minecraft:no_op"
}
@@ -11,7 +11,7 @@
"forge:condition": {
"type": "forge:false"
},
"rolls": 1.0
"rolls": 1
},
{
"entries": [
@@ -23,7 +23,7 @@
"forge:condition": {
"type": "forge:true"
},
"rolls": 1.0
"rolls": 1
}
],
"random_sequence": "conditional_loot_test:blocks/test"
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:false"
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,76 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:children": [
{
"forge:condition": {
"type": "forge:false"
}
},
{
"child": {
"parent": "minecraft:recipes/root",
"criteria": {
"has_oak_log": {
"conditions": {
"items": [
{
"items": "minecraft:oak_log"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_oak_log"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
},
"forge:condition": {
"type": "forge:true"
}
}
],
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:false"
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_bee_nest": {
"conditions": {
"items": [
{
"items": "minecraft:bee_nest"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:true"
},
"requirements": [
[
"has_the_recipe",
"has_bee_nest"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:false"
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_oak_log": {
"conditions": {
"items": [
{
"items": "minecraft:oak_log"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:true"
},
"requirements": [
[
"has_the_recipe",
"has_oak_log"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_redstone_ore": {
"conditions": {
"items": [
{
"items": "minecraft:redstone_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:false"
},
"requirements": [
[
"has_the_recipe",
"has_redstone_ore"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_redstone_ore": {
"conditions": {
"items": [
{
"items": "minecraft:redstone_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:true"
},
"requirements": [
[
"has_the_recipe",
"has_redstone_ore"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_redstone_ore": {
"conditions": {
"items": [
{
"items": "minecraft:redstone_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:true"
},
"requirements": [
[
"has_the_recipe",
"has_redstone_ore"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,35 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:false"
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,36 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_iron_ore": {
"conditions": {
"items": [
{
"items": "minecraft:iron_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:tag_empty",
"tag": "minecraft:dirt"
},
"requirements": [
[
"has_the_recipe",
"has_iron_ore"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,36 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_gold_ore": {
"conditions": {
"items": [
{
"items": "minecraft:gold_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:tag_empty",
"tag": "conditional_recipe:empty_tag_for_testing"
},
"requirements": [
[
"has_the_recipe",
"has_gold_ore"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -0,0 +1,63 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_dirt": {
"conditions": {
"items": [
{
"items": "minecraft:dirt"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "minecraft:diamond_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"forge:condition": {
"type": "forge:and",
"values": [
{
"type": "forge:or",
"values": [
{
"type": "forge:not",
"value": {
"type": "forge:mod_loaded",
"modid": "minecraft"
}
},
{
"type": "forge:item_exists",
"item": "minecraft:dirt"
},
{
"type": "forge:true"
},
{
"type": "forge:tag_empty",
"tag": "minecraft:dirt"
}
]
},
{
"type": "forge:false"
}
]
},
"requirements": [
[
"has_the_recipe",
"has_dirt"
]
],
"rewards": {
"recipes": [
"minecraft:diamond_block"
]
}
}
@@ -7,6 +7,7 @@
{
"recipe": {
"type": "minecraft:smelting",
"cookingtime": 200,
"experience": 0.1,
"ingredient": "minecraft:dirt",
"result": {
@@ -7,6 +7,7 @@
{
"recipe": {
"type": "minecraft:smelting",
"cookingtime": 200,
"experience": 0.1,
"ingredient": "minecraft:bee_nest",
"result": {
Loaded 100 of 890 files, more files were not shown because too many files have changed in this diff. Show more