Compare commits

..
20 Commits
Author SHA1 Message Date
LexManos 664a2c06ad 1.19.4 RB 2 2023-09-21 11:52:10 -07:00
Paint_Ninjaandmalte0811 e85295a837 Fix Level leak in debug HUD (#9700)
Co-authored-by: malte0811 <[email protected]>
2023-09-02 17:02:03 -07:00
Paint_Ninjaanddhyces 21582ef641 Keep order of sources in PackRepository (#9703)
Co-authored-by: dhyces <[email protected]>
2023-09-02 16:21:21 -07:00
Paint_Ninja 020ec3d51b Fix DelegatingPackResources searching resource path twice (#9698) 2023-08-30 14:10:38 -07:00
Jonathan Colmenares 774b79e609 Fix parameter names in IForgeDimensionSpecialEffects.adjustLightmapColors to better describe what they are. (#9657) 2023-07-17 09:45:29 -07:00
Jonathan Colmenares ee6022b006 Add mods button to pause menu (#9654) 2023-07-17 08:41:11 -07:00
brandon3055 37efd2b8ae Fixed FluidUtil#tryFillContainer returning invalid result when simulating #9358 2023-07-15 10:52:26 -07:00
Random832 cf3d42ef38 Fix conflicting keybindings not having the correct click count set. #9360 2023-07-15 10:44:29 -07:00
RealMangoRage 03a505a3f2 Fixed AdvancementsScreen.java to render next/previous page buttons now. (#9650) 2023-07-14 19:22:33 -07:00
ChampionAsh5357 bdc856edf5 Properly Handle Fluid Updates while in a Boat Fix #9256 (#9428) 2023-07-14 19:06:07 -07:00
Silver_David 6eebea0896 New hook in IClientBlockExtensions to handle enabling tinting on breaking particles. #9446 2023-07-14 16:08:51 -07:00
Technici4n 9218e08955 Add EmptyEnergyStorage (#9487) 2023-07-14 11:34:12 -07:00
Paint_Ninja 7006fe4f29 Support IPv6 address compression for logged IPs 2023-07-14 11:30:57 -07:00
Paint_Ninja 301e7252b0 Add option to advertise dedicated servers to LAN. (#9502) 2023-07-14 07:07:36 -07:00
ChampionAsh5357 2b5852072c Add particle description data provider (#9510) 2023-07-13 19:35:07 -07:00
bconlon 0b38373a45 AlterGroundEvent for modifying block placement performed by AlterGroundDecorator (#9636) 2023-07-13 17:30:45 -07:00
Paint_Ninja 07e538f3dd Improve logging for server connections (#9622) 2023-07-11 11:04:45 -07:00
cech12 b39e300c1b Fix placing fluids into waterlogged blocks with FluidUtil::tryPlaceFluid. To behave more like vanilla MC-127110 (#9519) 2023-07-11 11:04:26 -07:00
LexManos 50db51a366 Add proper duplicate mod error message that includes the mod id(s) and mod files. (#9474) 2023-06-20 14:18:14 -07:00
LexManos df80acc49d Make 1.20.x the main TC branch. 2023-06-20 14:07:42 -07:00
3805 changed files with 89164 additions and 80573 deletions

No files matched your search

-4
View File
@@ -3,7 +3,6 @@
*.patch text eol=lf
*.java text eol=lf
*.gradle text eol=crlf
**/MANIFEST.MF text eol=crlf
*.png binary
*.gif binary
*.exe binary
@@ -22,6 +21,3 @@ src/generated/**/*.json text eol=lf
src/generated_test/**/.cache/cache text eol=lf
src/generated_test/**/*.json text eol=lf
# We force eclipse to use unix line endings
ide/eclipse/template/.settings/*.prefs text eol=lf
+1 -1
View File
@@ -10,4 +10,4 @@ assignees: ''
<!-- Please note, feature requests that are submitted as GitHub issues here are rarely implemented. -->
<!-- If you'd like to see something in Forge, you should be ready to implement it as a PR. -->
<!-- If you're not sure, open a Draft PR or discuss it in the Discord server. -->
<!-- Discord: https://discord..minecraftforge.net/ -->
<!-- Discord: https://discord.gg/UvedJ9m -->
-1
View File
@@ -11,7 +11,6 @@ assignees: ''
<!-- For support with crashes and issues, please create a post on the forum instead: -->
<!-- https://www.minecraftforge.net/forum/forum/18-support-bug-reports/ -->
<!-- You can also use the discord for support, https://discord.minecraftforge.net/ -->
**Minecraft Version:** {Minecraft version}
-28
View File
@@ -1,28 +0,0 @@
name: Publish
on:
push:
branches: [ '26.1.1' ]
permissions:
contents: read
jobs:
build:
uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@main
with:
java: 21
gradle_setup: './gradlew setup'
gradle_tasks: 'build publish'
artifact_name: 'forge'
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }}
PROMOTE_ARTIFACT_USERNAME: ${{ secrets.PROMOTE_ARTIFACT_USERNAME }}
PROMOTE_ARTIFACT_PASSWORD: ${{ secrets.PROMOTE_ARTIFACT_PASSWORD }}
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS }}
SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
SIGN_KEYSTORE_PASSWORD: ${{ secrets.SIGN_KEYSTORE_PASSWORD }}
SIGN_KEYSTORE_DATA: ${{ secrets.SIGN_KEYSTORE_DATA }}
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
name: Upload payload
runs-on: ubuntu-latest
steps:
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
with:
name: payload
path: ${{ github.event_path }}
@@ -7,7 +7,7 @@ jobs:
name: Upload payload
runs-on: ubuntu-latest
steps:
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
with:
name: payload
path: ${{ github.event_path }}
+14 -14
View File
@@ -13,9 +13,6 @@
/classes
*.iml
# We however WANT the ide folder as it has tempaltes/configs for developing Forge.
!ide/**
#gradle
/build
/.gradle
@@ -26,6 +23,8 @@
/projects/mcp/
/projects/clean/
/projects/forge/
/projects/fmlonly/
#/patches/*
#occupational hazards
/projects/**/build/
@@ -33,17 +32,18 @@
/projects/**/run/
/projects/**/*.launch
/repo/
src/*/generated/**/.cache/
src/generated/resources/.cache/
src/generated_test/resources/.cache/
src/generated_mdk/resources/.cache/
# Generated by gradle every import
**/src/main/resources/META-INF/MANIFEST.MF
/fmlloader/src/main/resources/forge_version.json
#Patch rejects
/patches-/
*.patch.rej
/projects/*/rejects/
/ForgeRoot.ipr
/ForgeRoot.iws
# FML changelog
changelog.txt
*.py
*.7z
/logs/
# APT stuff
/*/.gitignore
/*/.factorypath
/*/.apt_generated/
/fmlcore/logs/
+104
View File
@@ -0,0 +1,104 @@
<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<name>MinecraftForge_Forge Config DSL Script</name>
<groupId>MinecraftForge_Forge</groupId>
<artifactId>MinecraftForge_Forge_dsl</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<repositories>
<repository>
<id>jetbrains-all</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>teamcity-server</id>
<url>https://teamcity.minecraftforge.net/app/dsl-plugins-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>JetBrains</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>${basedir}</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration/>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>teamcity-configs-maven-plugin</artifactId>
<version>${teamcity.dsl.version}</version>
<configuration>
<format>kotlin</format>
<dstDir>target/generated-configs</dstDir>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin</artifactId>
<version>${teamcity.dsl.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-script-runtime</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
+208
View File
@@ -0,0 +1,208 @@
import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.githubIssues
import jetbrains.buildServer.configs.kotlin.*
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
/*
The settings script is an entry point for defining a TeamCity
project hierarchy. The script should contain a single call to the
project() function with a Project instance or an init function as
an argument.
VcsRoots, BuildTypes, Templates, and subprojects can be
registered inside the project using the vcsRoot(), buildType(),
template(), and subProject() methods respectively.
To debug settings scripts in command-line, run the
mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
command and attach your debugger to the port 8000.
To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View
-> Tool Windows -> Maven Projects), find the generate task node
(Plugins -> teamcity-configs -> teamcity-configs:generate), the
'Debug' option is available in the context menu for the task.
*/
version = "2021.2"
project {
buildType(Build)
buildType(BuildSecondaryBranches)
buildType(PullRequests)
buildType(PullRequestChecks)
buildType(PullRequestCompatibility)
params {
text("docker_jdk_version", "17", label = "Gradle version", description = "The version of the JDK to use during execution of tasks in a JDK.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("docker_gradle_version", "7.5.1", label = "Gradle version", description = "The version of Gradle to use during execution of Gradle tasks.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("git_main_branch", "1.20.x", label = "Git Main Branch", description = "The git main or default branch to use in VCS operations.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("git_branch_spec", """
+:refs/heads/(main*)
+:refs/heads/(master*)
+:refs/heads/(develop|release|staging|main|master)
+:refs/heads/(1.*)
""".trimIndent(), label = "The branch specification of the repository", description = "By default all main branches are build by the configuration. Modify this value to adapt the branches build.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
text("github_repository_name", "MinecraftForge", label = "The github repository name. Used to connect to it in VCS Roots.", description = "This is the repository slug on github. So for example `MinecraftForge` or `MinecraftForge`. It is interpolated into the global VCS Roots.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("env.PUBLISHED_JAVA_ARTIFACT_ID", "forge", label = "Published artifact id", description = "The maven coordinate artifact id that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_FML_ARTIFACT_ID", "fmlonly", label = "Published fmlonly artifact id", description = "The maven coordinate artifact id for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION", "0.0.0-SNAPSHOT", label = "Published fmlonly artifact version", description = "The version for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_GROUP", "net.minecraftforge", label = "Published group", description = "The maven coordinate group that has been published by this build. Can not be empty.", allowEmpty = false)
//These are references and not actually keys
password("env.CROWDIN_KEY", "credentialsJSON:a3102dbe-805d-4177-9f54-3d2c2eb08fd5", display = ParameterDisplay.HIDDEN)
password("env.KEYSTORE_URL", "credentialsJSON:a7ae1c82-8058-4061-8d12-7f6bc2618d2e", display = ParameterDisplay.HIDDEN)
password("env.KEYSTORE_PASSWORD", "credentialsJSON:d7b964e3-a1fd-47a8-b892-6f601fe47479", display = ParameterDisplay.HIDDEN)
text("additional_publishing_gradle_parameters", "-PcrowdinKey=%env.CROWDIN_KEY% -PkeystoreKeyPass=%env.KEYSTORE_PASSWORD% -PkeystoreStorePass=%env.KEYSTORE_PASSWORD% -Pkeystore=%env.KEYSTORE_URL%", label = "Additional gradle parameters for publish", description = "Contains the additional gradle parameters used during publishing.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
checkbox("should_execute_build", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
checkbox("should_execute_test", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
}
features {
githubIssues {
id = "MinecraftForge_MinecraftForge__IssueTracker"
displayName = "MinecraftForge/MinecraftForge"
repositoryURL = "https://github.com/MinecraftForge/MinecraftForge"
}
}
}
object Build : BuildType({
templates(AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"), AbsoluteId("MinecraftForge_PublishProjectUsingGradle"), AbsoluteId("MinecraftForge_TriggersStaticFilesWebpageGenerator"))
id("MinecraftForge_MinecraftForge__Build")
name = "Build"
description = "Builds and Publishes the main branches of the project."
features {
feature {
id = "trigger_fml_only_files_generator"
type = "triggerBuildFeature"
param("triggers", "MinecraftForge_FilesGenerator_GeneratePages")
param("parameters", """
env.PUBLISHED_JAVA_FML_ARTIFACT_ID~env.PUBLISHED_JAVA_ARTIFACT_ID
env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION~env.PUBLISHED_JAVA_ARTIFACT_VERSION
env.PUBLISHED_JAVA_GROUP
""".trimIndent())
}
}
})
object BuildSecondaryBranches : BuildType({
templates(AbsoluteId("MinecraftForge_ExcludesBuildingDefaultBranch"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
id("MinecraftForge_MinecraftForge__BuildSecondaryBranches")
name = "Build - Secondary Branches"
description = "Builds and Publishes the secondary branches of the project."
params {
text("git_branch_spec", """
+:refs/heads/(*)
-:refs/heads/noci*
""".trimIndent(), label = "The branch specification of the repository", description = "By default all main branches are build by the configuration. Modify this value to adapt the branches build.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequests : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequests")
name = "Pull Requests"
description = "Builds pull requests for the project"
params {
checkbox("should_execute_build", "true", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
text(
"gradle_build_task",
"assemble",
label = "Gradle build task to execute during build",
description = "Determines the build task that is executed to build the project.",
display = ParameterDisplay.HIDDEN,
allowEmpty = false
)
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequestChecks : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequestChecks")
name = "Pull Requests (Checks)"
description = "Checks pull requests for the project"
steps {
gradle {
name = "Check"
id = "RUNNER_10_Check"
tasks = "checkAll"
gradleParams = "--continue %gradle_custom_args%"
enableStacktrace = true
dockerImage = "%docker_gradle_image%"
dockerRunParameters = """
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
--network=host
-u 1000:1000
%docker_additional_args%
""".trimIndent()
}
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequestCompatibility : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequestCompatibility")
name = "Pull Requests (Compatibility)"
description = "Validates binary compatibility for pull requests made to the project"
steps {
gradle {
name = "Validate"
id = "RUNNER_10_Compatibility"
tasks = "checkJarCompatibility"
gradleParams = "--continue %gradle_custom_args%"
enableStacktrace = true
dockerImage = "%docker_gradle_image%"
dockerRunParameters = """
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
--network=host
-u 1000:1000
%docker_additional_args%
""".trimIndent()
}
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
+65
View File
@@ -0,0 +1,65 @@
Minecraft Forge: Credits/Thank You
Forge is a set of tools and modifications to the Minecraft base game code to assist
mod developers in creating new and exciting content. It has been in development for
several years now, but I would like to take this time thank a few people who have
helped it along it's way.
First, the people who originally created the Forge projects way back in Minecraft
alpha. Eloraam of RedPower, and SpaceToad of Buildcraft, without their acceptiance
of me taking over the project, who knows what Minecraft modding would be today.
Secondly, someone who has worked with me, and developed some of the core features
that allow modding to be as functional, and as simple as it is, cpw. For developing
FML, which stabelized the client and server modding ecosystem. As well as the base
loading system that allows us to modify Minecraft's code as elegently as possible.
Mezz, who has stepped up as the issue and pull request manager. Helping to keep me
sane as well as guiding the community into creating better additions to Forge.
Searge, Bspks, Fesh0r, ProfMobious, and all the rest over on the MCP team {of which
I am a part}. For creating some of the core tools needed to make Minecraft modding
both possible, and as stable as can be.
On that note, here is some specific information of the MCP data we use:
* Minecraft Coder Pack (MCP) *
Forge Mod Loader and Minecraft Forge have permission to distribute and automatically
download components of MCP and distribute MCP data files. This permission is not
transitive and others wishing to redistribute the Minecraft Forge source independently
should seek permission of MCP or remove the MCP data files and request their users
to download MCP separately.
And lastly, the countless community members who have spent time submitting bug reports,
pull requests, and just helping out the community in general. Thank you.
--LexManos
=========================================================================
This is Forge Mod Loader.
You can find the source code at all times at https://github.com/MinecraftForge/MinecraftForge/tree/1.12.x/src/main/java/net/minecraftforge/fml
This minecraft mod is a clean open source implementation of a mod loader for minecraft servers
and minecraft clients.
The code is authored by cpw.
It began by partially implementing an API defined by the client side ModLoader, authored by Risugami.
http://www.minecraftforum.net/topic/75440-
This support has been dropped as of Minecraft release 1.7, as Risugami no longer maintains ModLoader.
It also contains suggestions and hints and generous helpings of code from LexManos, author of MinecraftForge.
http://www.minecraftforge.net/
Additionally, it contains an implementation of topological sort based on that
published at http://keithschwarz.com/interesting/code/?dir=topological-sort
It also contains code from the Maven project for performing versioned dependency
resolution. http://maven.apache.org/
It also contains a partial repackaging of the javaxdelta library from http://sourceforge.net/projects/javaxdelta/
with credit to it's authors.
Forge Mod Loader downloads components from the Minecraft Coder Pack
(http://mcp.ocean-labs.de/index.php/Main_Page) with kind permission from the MCP team.
Vendored
+125
View File
@@ -0,0 +1,125 @@
@Library('forge-shared-library')_
pipeline {
options {
disableConcurrentBuilds()
}
agent {
docker {
image 'gradle:7-jdk16'
//args '-v forgegc:/home/gradle/.gradle_cache/'
}
}
environment {
GRADLE_ARGS = '--no-daemon --console=plain' // No daemon for now as FG3 kinda derps. //'-Dorg.gradle.daemon.idletimeout=5000'
DISCORD_WEBHOOK = credentials('forge-discord-jenkins-webhook')
DISCORD_PREFIX = "Job: Forge Branch: ${BRANCH_NAME} Build: #${BUILD_NUMBER}"
JENKINS_HEAD = 'https://wiki.jenkins-ci.org/download/attachments/2916393/headshot.png'
}
stages {
stage('notify_start') {
when {
not {
changeRequest()
}
}
steps {
discordSend(
title: "${DISCORD_PREFIX} Started",
successful: true,
result: 'ABORTED', //White border
thumbnail: JENKINS_HEAD,
webhookURL: DISCORD_WEBHOOK
)
}
}
/*
stage('pull_cache') {
steps {
sh 'cp -r /home/gradle/.gradle_cache/jdks/ /home/gradle/.gradle/jdks/'
sh 'cp -r /home/gradle/.gradle_cache/wrapper/ /home/gradle/.gradle/wrapper/'
sh 'cp -r /home/gradle/.gradle_cache/caches/ /home/gradle/.gradle/caches/'
}
}
*/
stage('setup') {
steps {
withGradle {
sh './gradlew ${GRADLE_ARGS} --refresh-dependencies --continue setup'
}
script {
env.MYVERSION = sh(returnStdout: true, script: './gradlew :forge:properties -q | grep "^version:" | cut -d" " -f2').trim()
env.FMLONLY_VERSION = sh(returnStdout: true, script: './gradlew :fmlonly:properties -q | grep "^version:" | cut -d" " -f2').trim()
}
}
}
stage('changelog') {
when {
not {
changeRequest()
}
}
steps {
writeChangelog(currentBuild, 'build/changelog.txt')
}
}
stage('publish') {
when {
not {
changeRequest()
}
}
environment {
CROWDIN = credentials('forge-crowdin')
KEYSTORE = credentials('forge-jenkins-keystore-old')
KEYSTORE_KEYPASS = credentials('forge-jenkins-keystore-old-keypass')
KEYSTORE_STOREPASS = credentials('forge-jenkins-keystore-old-keypass')
}
steps {
withCredentials([usernamePassword(credentialsId: 'maven-forge-user', usernameVariable: 'MAVEN_USER', passwordVariable: 'MAVEN_PASSWORD')]) {
withGradle {
sh './gradlew ${GRADLE_ARGS} publish -PkeystoreKeyPass=${KEYSTORE_KEYPASS} -PkeystoreStorePass=${KEYSTORE_STOREPASS} -Pkeystore=${KEYSTORE} -PcrowdinKey=${CROWDIN}'
}
}
}
post {
success {
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:forge ${env.MYVERSION} latest")], propagate: false, wait: false
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:fmlonly ${env.FMLONLY_VERSION} latest")], propagate: false, wait: false
}
}
}
stage('test_publish_pr') { //Publish to local repo to test full process, but don't include credentials so it can't sign/publish to maven
when {
changeRequest()
}
environment {
CROWDIN = credentials('forge-crowdin')
}
steps {
withGradle {
sh './gradlew ${GRADLE_ARGS} publish -PcrowdinKey=${CROWDIN}'
}
}
}
}
post {
always {
script {
archiveArtifacts artifacts: 'projects/forge/build/libs/**/*.*', fingerprint: true, onlyIfSuccessful: true, allowEmptyArchive: true
if (env.CHANGE_ID == null) { // This is unset for non-PRs
discordSend(
title: "${DISCORD_PREFIX} Finished ${currentBuild.currentResult}",
description: '```\n' + getChanges(currentBuild) + '\n```',
successful: currentBuild.resultIsBetterOrEqualTo("SUCCESS"),
result: currentBuild.currentResult,
thumbnail: JENKINS_HEAD,
webhookURL: DISCORD_WEBHOOK
)
}
}
}
}
}
-3
View File
@@ -57,9 +57,6 @@ This software contains a partial repackaging of javaxdelta, a BSD licensed progr
binary differences and applying them, sourced from the subversion at http://sourceforge.net/projects/javaxdelta/
authored by genman, heikok, pivot.
The only changes are to replace some Trove collection types with standard Java collections, and repackaged.
This software includes the Monocraft font from https://github.com/IdreesInc/Monocraft/ for use in the early loading
display.
=========================================================================
+1763 -27
View File
File diff suppressed because it is too large. Load diff
+8 -9
View File
@@ -1,14 +1,13 @@
repositories {
maven { url = 'https://maven.minecraftforge.net/' }
mavenCentral()
mavenCentral()
}
dependencies {
implementation 'org.ow2.asm:asm:9.9.1'
implementation 'org.ow2.asm:asm-tree:9.9.1'
implementation 'net.minecraftforge:srgutils:0.6.2'
implementation 'net.minecraftforge:JarJarMetadata:0.3.27'
implementation 'commons-io:commons-io:2.13.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.7.0.202309050840-r'
}
implementation 'org.ow2.asm:asm:9.5'
implementation 'org.ow2.asm:asm-tree:9.5'
implementation 'net.minecraftforge:srgutils:0.5.+'
implementation 'commons-io:commons-io:2.8.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.5.0.202303070854-r'
}
@@ -1,67 +0,0 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.tasks.*
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.util.zip.ZipFile
abstract class BundleList extends DefaultTask {
@InputFiles abstract ConfigurableFileCollection getConfig()
@InputFile abstract RegularFileProperty getServerBundle()
@OutputFile abstract RegularFileProperty getOutput()
BundleList() {
config.setFrom(project.configurations.installer)
output.convention(project.layout.buildDirectory.file("$name/output.list"))
configure {
dependsOn(project.tasks.universalJar)
inputs.file(project.tasks.universalJar.archiveFile)
dependsOn(project.tasks.applyServerBinPatches)
inputs.file(project.tasks.applyServerBinPatches.output)
}
}
@TaskAction
void run() {
def entries = [:] as TreeMap
def resolved = project.configurations.installer.resolvedConfiguration.resolvedArtifacts
for (def dep : resolved) {
def info = Util.getMavenInfoFromDep(dep)
//println("$dep.file.sha1\t$info.name\t$info.path")
entries.put("$info.art.group:$info.art.name", "$dep.file.sha256\t$info.name\t$info.path")
}
var packed = (AbstractArchiveTask) project.tasks.universalJar
var info = Util.getMavenInfoFromTask(packed)
def file = packed.archiveFile.get().asFile
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
var classifier = 'server'
var genned = project.tasks.applyServerBinPatches
info = Util.getMavenInfoFromTask(genned, classifier)
file = genned.output.get().asFile
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
try (def zip = new ZipFile(serverBundle.get().asFile)) {
def entry = zip.getEntry('META-INF/libraries.list')
def data = zip.getInputStream(entry).text.split('\n')
for (def line : data) {
def (sha, artifact, path) = line.split('\t')
def (group, name, other) = artifact.split(':', 3)
//println("Group: $group Name: $name")
def key = "$group:$name"
if (other.indexOf(':') != -1){
key += ':' + other.split(':', 2)[1]
//println("\tKey: $key")
}
if (!entries.containsKey(key))
entries.put(key, line)
}
}
output.get().asFile.text = entries.values().join('\n')
}
}
@@ -1,22 +1,18 @@
package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
@CompileStatic
abstract class BytecodeFinder extends DefaultTask {
@InputFile abstract RegularFileProperty getJar()
// It should be fine to mark the output as internal as we want to control when we run it anyways.
// This also shuts Gradle 8 up about implicit task dependencies.
@Internal abstract RegularFileProperty getOutput()
@OutputFile abstract RegularFileProperty getOutput()
BytecodeFinder() {
output.convention(project.layout.buildDirectory.dir(name).map { it.file("output.json") })
@@ -26,13 +22,13 @@ abstract class BytecodeFinder extends DefaultTask {
protected void exec() {
Util.init()
var outputFile = output.get().asFile
def outputFile = output.get().asFile
if (outputFile.exists())
outputFile.delete()
pre()
Util.processClassNodes(jar.get().asFile, this.&process)
Util.processClassNodes(jar.get().asFile, this::process)
post()
outputFile.text = new JsonBuilder(getData()).toPrettyString()
@@ -40,8 +36,8 @@ abstract class BytecodeFinder extends DefaultTask {
protected process(ClassNode node) {
if (node.fields !== null) node.fields.each { process(node, it) }
if (node.methods !== null) node.methods.each { process(node, it) }
if (node.fields != null) node.fields.each { process(node, it) }
if (node.methods != null) node.methods.each { process(node, it) }
}
protected pre() {}
@@ -3,7 +3,6 @@ package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@@ -13,19 +12,13 @@ abstract class BytecodePredicateFinder extends BytecodeFinder {
@Internal
abstract Property<Closure<Boolean>> getPredicate()
private final Map<String, List<String>> matches = new TreeMap<>(Comparator.naturalOrder())
private final Set<String> matches = new HashSet()
@Override
protected process(ClassNode parent, MethodNode node) {
for (final current : node.instructions) {
if (predicate.get().call(parent, node, current)) {
var methods = matches.compute(parent.name, { k, v -> v ?: new ArrayList<>() })
// only add non-synthetic methods
if ((node.access & Opcodes.ACC_SYNTHETIC) == 0) {
methods.add(node.name + node.desc)
}
matches.add(parent.name)
return
}
}
@@ -34,15 +27,6 @@ abstract class BytecodePredicateFinder extends BytecodeFinder {
@Internal
@Override
protected Object getData() {
var array = new ArrayList<Object>()
matches.forEach { c, m ->
{
array.add(
'class': c,
'methods': m
)
}
}
return array ?: { throw new RuntimeException('Failed to find any targets, please ensure that method names and descriptors are correct.') }()
return matches.toSorted(Comparator.naturalOrder()) ?: {throw new RuntimeException('Failed to find any targets, please ensure that method names and descriptors are correct.')}()
}
}
@@ -1,97 +0,0 @@
package net.minecraftforge.forge.tasks;
import groovy.transform.CompileDynamic;
import groovy.transform.CompileStatic;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeSet;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
/**
* Eclipse config files are literally just java properties, with the header cleaned up.
* https://github.com/eclipse/buildship/blob/5b2c7fca7fa86cd74d71b3c099c7b1559eba038e/org.eclipse.buildship.core/src/main/java/org/eclipse/buildship/core/internal/configuration/PreferenceStore.java#L238
*
* This does the same thing, as well as sorting alphabetically.
* It also ignores all comments. We can add them latter if someone cares.
*/
@CompileStatic
public class CleanProperties extends Properties {
private static final long serialVersionUID = 1L;
private static final String LINE_SEP = System.getProperty("line.separator");
private static final String UNIX_LINE_SEP = "\n";
private static final Charset ENCODING = StandardCharsets.UTF_8;
public CleanProperties load(File input) throws IOException {
if (input.exists()) {
try (Reader is = new InputStreamReader(new FileInputStream(input), ENCODING)) {
super.load(is);
}
}
return this;
}
public void store(File out) throws IOException {
if (!out.getParentFile().exists())
out.getParentFile().mkdirs();
try (OutputStream os = new FileOutputStream(out)) {
store(os, null);
}
}
@Override
public synchronized Enumeration<Object> keys() {
Set<Object> ret = new TreeSet<>();
for (Enumeration<?> e = super.keys(); e.hasMoreElements();)
ret.add(e.nextElement());
return Collections.enumeration(ret);
}
@CompileDynamic
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
Set<Map.Entry<Object, Object>> ret = new TreeSet<>((l, r) -> ((String)l.getKey()).compareTo((String)r.getKey()));
ret.addAll(super.entrySet());
return ret;
}
@Override
public void store(OutputStream out, String comments) throws IOException {
out.write(clean().getBytes(ENCODING));
out.flush();
}
@Override
public void store(Writer out, String comments) throws IOException {
out.write(clean());
out.flush();
}
private String clean() throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
try {
super.store(tmp, null);
} finally {
tmp.close();
}
String ret = tmp.toString(ENCODING).replace(LINE_SEP, UNIX_LINE_SEP);
ret = ret.substring(ret.indexOf(UNIX_LINE_SEP) + 1);
return ret;
}
}
@@ -1,7 +1,5 @@
package net.minecraftforge.forge.tasks
import groovy.transform.stc.FirstParam
import java.util.function.BiConsumer
public class ClosureHelper {
@@ -24,7 +22,7 @@ public class ClosureHelper {
}
}
static <T> T apply(T obj, @DelegatesTo(value = FirstParam, strategy = Closure.DELEGATE_FIRST) Closure cl) {
static <T> T apply(T obj, Closure cl) {
cl.delegate = obj
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
@@ -0,0 +1,73 @@
package net.minecraftforge.forge.tasks
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.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
abstract class Crowdin extends DefaultTask {
@Input abstract Property<String> getId()
@Input @Optional abstract Property<String> getKey()
@Input boolean json = true
@OutputFile abstract RegularFileProperty getOutput()
@OutputFile abstract RegularFileProperty getExport()
Crowdin() {
outputs.upToDateWhen{ false }
id.convention('minecraft-forge')
output.convention(project.layout.buildDirectory.dir(name).map {it.file("output.zip") })
export.convention(project.layout.buildDirectory.dir(name).map {it.file("export.json") })
}
@TaskAction
def run() {
File outputFile = output.get().asFile
File exportFile = export.get().asFile
if (outputFile.exists())
outputFile.delete()
if (!key.isPresent())
return
String key = this.key.get()
String id = this.id.get()
// Force an export
new URL("https://api.crowdin.com/api/project/${id}/export?key=${key}").withInputStream { i ->
exportFile.withOutputStream { it << i }
}
if (!exportFile.text.contains('success')) {
throw new RuntimeException("Crowdin export failed, see ${exportFile} for more info")
}
new URL("https://api.crowdin.com/api/project/${id}/download/all.zip?key=${key}").withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
outputFile.withOutputStream { out ->
new ZipOutputStream(out).withCloseable { zout ->
ZipEntry zein
while ((zein = zin.nextEntry) != null) {
if (zein.isDirectory()) {
zout.putNextEntry(new ZipEntry(zein.name))
} else {
// 1.13+ uses json
if (zein.name.endsWith('.json') == json) {
ZipEntry zeout = new ZipEntry(json ? zein.name.toLowerCase() : zein.name)
zeout.time = 1
zout.putNextEntry(zeout)
zout << zin
}
}
}
}
}
}
}
}
}
@@ -27,7 +27,7 @@ abstract class DownloadLibraries extends DefaultTask {
def run() {
Util.init()
File outputDir = output.get().asFile
var libraries = new ArrayList<String>()
def libraries = new ArrayList()
def json = input.get().asFile.json().libraries.each { lib ->
//TODO: Thread?
@@ -35,7 +35,7 @@ abstract class DownloadLibraries extends DefaultTask {
artifacts.each{ art ->
def target = new File(outputDir, art.path)
libraries.add(target.absolutePath)
if (!target.exists() || art.sha1 != target.sha1()) {
if (!target.exists() || !art.sha1.equals(target.sha1())) {
project.logger.lifecycle("Downloading ${art.url}")
if (!target.parentFile.exists()) {
target.parentFile.mkdirs()
@@ -43,7 +43,7 @@ abstract class DownloadLibraries extends DefaultTask {
new URL(art.url).withInputStream { i ->
target.withOutputStream { it << i }
}
if (art.sha1 != target.sha1()) {
if (!art.sha1.equals(target.sha1())) {
throw new IllegalStateException("Failed to download ${art.url} to ${target.canonicalPath} SHA Mismatch")
}
}
@@ -1,28 +0,0 @@
package net.minecraftforge.forge.tasks
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.*
import java.util.zip.ZipFile
@CompileStatic
abstract class ExtractFile extends DefaultTask {
@InputFile abstract RegularFileProperty getInput()
@Input abstract Property<String> getTarget()
@OutputFile abstract RegularFileProperty getOutput()
@TaskAction
protected void exec() {
var zip = new ZipFile(input.get().asFile);
var entry = zip.getEntry(target.get())
if (entry == null)
throw new IllegalStateException(input.get().asFile.absolutePath + " does not contain " + target.get())
var stream = zip.getInputStream(entry)
output.get().asFile.withOutputStream { out ->
stream.transferTo(out)
}
}
}
@@ -1,11 +1,20 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import java.util.ArrayList
import java.util.HashMap
import java.util.TreeMap
import java.util.TreeSet
import java.util.function.BiConsumer
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
import static org.objectweb.asm.Opcodes.*
@@ -25,17 +34,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
@Override
protected process(ClassNode parent, MethodNode node) {
AbstractInsnNode last = null
def last = null
def parentInstance = new ObjectTarget(owner: parent.name, name: '', desc: '')
for (int x = 0; x < node.instructions.size(); x++) {
def current = node.instructions.get(x)
if (current.opcode === IF_ACMPEQ || current.opcode === IF_ACMPNE) {
if (last !== null && (last.opcode === GETSTATIC || last.opcode === GETFIELD)) {
if (current.opcode == IF_ACMPEQ || current.opcode == IF_ACMPNE) {
if (last != null && (last.opcode == GETSTATIC || last.opcode == GETFIELD)) {
def target = new Search(cls: last.owner, name: last.name)
def wanted = fieldsReverse.get(target)
def original = fields.get(wanted)
def instance = new ObjectTarget(owner: parent.name, name: node.name, desc: node.desc)
if (wanted !== null && (original.blacklist === null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
if (wanted != null && (original.blacklist == null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
targets.computeIfAbsent(wanted, { k -> new TreeSet() }).add(instance)
}
}
@@ -59,10 +68,9 @@ abstract class FieldCompareFinder extends BytecodeFinder {
}
return ret
}
@CompileStatic
@EqualsAndHashCode(excludes = ['replacement', 'blacklist'])
static class Search {
public static class Search {
@Input
String cls
@@ -81,17 +89,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
return cls + '.' + name
}
def blacklist(String owner, String name, String desc) {
if (blacklist === null)
blacklist = new HashSet<>()
def blacklist(def owner, def name, def desc) {
if (blacklist == null)
blacklist = new HashSet()
blacklist.add(new ObjectTarget(owner: owner, name: name, desc: desc))
}
def blacklist(String owner) {
def blacklist(def owner) {
blacklist(owner, '', '')
}
}
void fields(Closure cl) {
def fields(Closure cl) {
new ClosureHelper(cl, {name, ccl ->
def search = ClosureHelper.apply(new Search(), ccl)
this.fields.put(name, search)
@@ -1,158 +1,44 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.*
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import groovy.json.JsonSlurper
import org.gradle.api.provider.SetProperty
abstract class InstallerJar extends Zip {
@Input @Optional abstract Property<Boolean> getFat()
@Input @Optional abstract Property<Boolean> getOffline()
@Input @Optional abstract SetProperty<String> getPackedDependencies()
InstallerJar() {
archiveClassifier.set('installer')
archiveExtension.set('jar') // Needs to be Zip task to not override Manifest, so set extension
destinationDirectory.set(project.layout.buildDirectory.dir('libs'))
fat.convention(false)
offline.convention(false)
def installerJson = project.tasks.installerJson
def launcherJson = project.tasks.launcherJson
def downloadInstaller = project.tasks.downloadInstaller
dependsOn(installerJson, launcherJson, downloadInstaller, project.configurations.installer)
dependsOn(installerJson, launcherJson, downloadInstaller)
from(installerJson, launcherJson)
from(project.rootProject.file('/src/main/resources/url.png'))
project.afterEvaluate {
from(project.zipTree(downloadInstaller.output)) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
duplicatesStrategy = 'exclude'
}
if (fat.get() || offline.get()) {
def cfg = project.tasks.register(name + "Config", Configure)
cfg.get().configure {
parent = this
dependsOn(project.tasks.installerJson, project.tasks.launcherJson)
if (!System.env.TEAMCITY_VERSION) {
dependsOn(project.universalJar)
from(project.universalJar) {
into '/maven/' + project.group.replace('.', '/') + '/' + project.universalJar.archiveBaseName.get() + '/' + project.version + '/'
}
dependsOn(cfg)
} else {
// Things we ALWAYS bundle, this just the server shim jar, because the installer spec only says to extract the file.
// I should make it allow downloads but thats a spec break, and this is just a ~14KB jar
[
project.tasks.serverShimJar // Server bootstrap executable jar
].forEach { AbstractArchiveTask packed ->
def path = Util.getMavenInfoFromTask(packed).path
from(packed) {
rename { "maven/$path" }
packedDependencies.get().forEach {
def jarTask = project.rootProject.getTasks().findByPath(it)
dependsOn(jarTask)
from(jarTask) {
into '/maven/' + jarTask.project.group.replace('.', '/') + '/' + jarTask.archiveBaseName.get() + '/' + jarTask.project.version + '/'
}
}
}
}
}
static abstract class Configure extends DefaultTask {
public Zip parent
private int count = 0;
@TaskAction
protected void exec() {
def deps = [:] as java.util.TreeMap
// Gather all things that need downloading
[
project.tasks.installerJson,
project.tasks.launcherJson
].each { task ->
def json = task.output.get().asFile.json
json.libraries.each { lib ->
if (lib.downloads?.artifact?.url !== null && !lib.downloads.artifact.url.isEmpty())
deps.put(lib.name, lib.downloads.artifact)
}
}
// First find things we build in this project.
[
project.tasks.universalJar, // Forge runtime code
project.tasks.serverShimJar // Shim jar for dedicated server
].forEach { AbstractArchiveTask packed ->
def name = Util.getMavenInfoFromTask(packed).name
def info = deps.remove(name)
if (info !== null) {
println("Adding: $packed.path $name")
parent.from(packed) {
rename { "maven/$info.path" }
}
}
}
// Find any artifacts from the 'installer' config
// This config specifies the runtime files we intend for the interaller to have.
// And are typically what we would be developing and testing alongside Forge.
// So we may have local modified versions
def cfg = project.configurations.installer
while (cfg != null) {
//println('')
def resolved = cfg.resolvedConfiguration.resolvedArtifacts
int found = 0
for (def dep : resolved) {
def name = Util.getMavenInfoFromDep(dep).name
def info = deps.remove(name)
if (info == null) {
//println("Skipping: $name")
continue
}
//println("-$name")
found++
addFile(dep.file, info)
}
if (deps.isEmpty()) {
cfg = null
continue
}
// Prevent infinite loops if something fucky happens
if (found == 0)
throw new IllegalStateException("Failed to find any installer dependencies")
def seen = [] as Set
cfg = project.configurations.detachedConfiguration()
cfg.transitive = false
for (def key : deps.keySet()) {
def (group, artifact, other) = key.split(':', 3)
// Only resolve unique group:artifact so we don't get version overrides
if (seen.add(group + ':' + artifact)) {
//println("+$key")
cfg.dependencies.add(project.dependencies.create(key))
}
}
}
}
void addFile(file, info) {
boolean pack = parent.offline.get() || info.url.isEmpty()
// If it's a offline jar just always pack
if (!pack) {
try {
var remote = new URL("${info.url}.sha1").getText('UTF-8')
pack = info.sha1 != remote
} catch (FileNotFoundException e) {
pack = !info.url.startsWith('https://libraries.minecraft.net/')
// Oh noes its not there!
}
}
if (pack) {
println("Adding: $file.absolutePath")
parent.from(file) {
rename { "maven/$info.path" }
}
}
}
}
}
@@ -7,13 +7,13 @@ import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.nio.file.Files
abstract class InstallerJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input @Optional abstract SetProperty<String> getPackedDependencies()
@Input @Optional final Map<String, Object> libraries = new LinkedHashMap<>()
@Input Map<String, Object> json = new LinkedHashMap<>()
@InputFile abstract RegularFileProperty getIcon()
@@ -23,19 +23,29 @@ abstract class InstallerJson extends DefaultTask {
@Input abstract Property<String> getWelcome()
InstallerJson() {
launcherJsonName.convention('/version.json')
logo.convention('/big_logo.png')
mirrors.convention('https://files.minecraftforge.net/mirrors-2.0.json')
welcome.convention("Welcome to the ${project.name.capitalize()} installer.")
output.convention(project.layout.buildDirectory.file('libs/install_profile.json'))
getLauncherJsonName().convention('/version.json')
getLogo().convention('/big_logo.png')
getMirrors().convention('https://files.minecraftforge.net/mirrors-2.0.json')
getWelcome().convention("Welcome to the simple ${project.name.capitalize()} installer.")
getOutput().convention(project.layout.buildDirectory.file('install_profile.json'))
['client', 'server'].each { side ->
['slim', 'extra'].each { type ->
def tsk = project.tasks.getByName("download${side.capitalize()}${type.capitalize()}")
dependsOn(tsk)
input.from(tsk.output)
}
def tsk = project.tasks.getByName("create${side.capitalize()}SRG")
dependsOn(tsk)
input.from(tsk.output)
}
project.afterEvaluate {
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { packed ->
dependsOn(packed)
input.from packed.archiveFile
(packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]).forEach {
dependsOn(it)
input.from it.archiveFile
}
}
}
@@ -43,19 +53,17 @@ abstract class InstallerJson extends DefaultTask {
@TaskAction
protected void exec() {
def libs = libraries
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { AbstractArchiveTask packed ->
def info = Util.getMavenInfoFromTask(packed)
libs.put(info.name, [
name: info.name,
for (def child : packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]) {
def dep = Util.getMavenDep(child)
def path = Util.getMavenPath(child)
libs.put(dep.toString(), [
name: dep,
downloads: [
artifact: [
path: info.path,
url: "https://maven.minecraftforge.net/$info.path",
sha1: packed.archiveFile.get().asFile.sha1(),
size: packed.archiveFile.get().asFile.length()
path: path,
url: "https://maven.minecraftforge.net/${path}",
sha1: child.archiveFile.get().asFile.sha1(),
size: child.archiveFile.get().asFile.length()
]
]
])
@@ -1,415 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.tasks;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.minecraftforge.jarjar.metadata.ContainedJarIdentifier;
import net.minecraftforge.jarjar.metadata.ContainedJarMetadata;
import net.minecraftforge.jarjar.metadata.ContainedVersion;
import net.minecraftforge.jarjar.metadata.Metadata;
import net.minecraftforge.jarjar.metadata.MetadataIOHandler;
import net.minecraftforge.jarjar.metadata.json.ArtifactVersionSerializer;
import net.minecraftforge.jarjar.metadata.json.ContainedJarIdentifierSerializer;
import net.minecraftforge.jarjar.metadata.json.ContainedJarMetadataSerializer;
import net.minecraftforge.jarjar.metadata.json.ContainedVersionSerializer;
import net.minecraftforge.jarjar.metadata.json.MetadataSerializer;
import net.minecraftforge.jarjar.metadata.json.VersionRangeSerializer;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.ExternalModuleDependency;
import org.gradle.api.artifacts.FileCollectionDependency;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.ModuleIdentifier;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.zip.ZipFile;
// TODO SUPER SUPER SUPER UGLY, CLEAN UP IN FORGEDEV 7
@Deprecated(forRemoval = true) // Will be moved to JarJar plugin in ForgeDev 7
public abstract class JarJarMetadataOptions extends DefaultTask {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(VersionRange.class, new VersionRangeSerializer())
.registerTypeAdapter(ArtifactVersion.class, new ArtifactVersionSerializer())
.registerTypeAdapter(DefaultArtifactVersion.class, new ArtifactVersionSerializer())
.registerTypeAdapter(ContainedJarIdentifier.class, new ContainedJarIdentifierSerializer())
.registerTypeAdapter(ContainedJarMetadata.class, new ContainedJarMetadataSerializer())
.registerTypeAdapter(ContainedVersion.class, new ContainedVersionSerializer())
.registerTypeAdapter(Metadata.class, new MetadataSerializer())
.setPrettyPrinting()
.create();
protected abstract @Input SetProperty<ResolvedDependencyInfoImpl> getResolvedDependencies();
protected abstract @OutputFile RegularFileProperty getMetadataFile();
protected abstract @Inject ProjectLayout getLayout();
// NOTE: I'm not adding a non-provider version. please just use the version catalog entries for now.
public void add(Provider<MinimalExternalModuleDependency> dependency, Action<? super ResolvedDependencyInfo> action) {
this.getResolvedDependencies().add(dependency.map(d -> {
var ret = ResolvedDependencyInfoImpl.from(this.getProject().getConfigurations(), d);
action.execute(ret);
return ret;
}));
}
@Inject
public JarJarMetadataOptions() {
this.getMetadataFile().convention(this.getLayout().getBuildDirectory().file(this.getName() + "/options.json"));
}
@TaskAction
protected void exec() {
record ForgeLocaterOptions(String resource, String layer, String id, List<ContainedJarMetadata> deps, ContainedJarMetadata meta, boolean nested) { }
var resolved = this.getResolvedDependencies().get();
var jars = new ArrayList<ForgeLocaterOptions>(resolved.size());
for (var dependency : resolved) {
var deps = new ArrayList<ContainedJarMetadata>();
try (var zip = new ZipFile(dependency.artifact)) {
var entry = zip.getEntry("META-INF/jarjar/metadata.json");
if (entry != null) {
try (var stream = zip.getInputStream(entry)) {
var meta = MetadataIOHandler.fromStream(stream).orElse(null);
if (meta == null)
throw new IllegalStateException("Corrupt metadata.json in " + dependency.artifact.getAbsolutePath());
for (var dep : meta.jars())
deps.add(new ContainedJarMetadata(dep.identifier(), dep.version(), "", dep.isObfuscated()));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
jars.add(new ForgeLocaterOptions(
dependency.resource,
dependency.layer,
dependency.identifier,
deps,
new ContainedJarMetadata(
new ContainedJarIdentifier(validateGroup(dependency), dependency.module.getName()),
new ContainedVersion(null, parseVersion(dependency)),
Objects.requireNonNull(dependency.path, "Dependency path is unspecified: " + dependency.asString),
false
),
dependency.nested
));
}
try {
record Meta(List<ForgeLocaterOptions> options){}
Files.writeString(
this.getMetadataFile().getAsFile().get().toPath(),
GSON.toJson(new Meta(jars), Meta.class)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String validateGroup(ResolvedDependencyInfoImpl dependency) {
try {
return Objects.requireNonNull(dependency.module.getGroup());
} catch (NullPointerException e) {
throw new IllegalArgumentException("Module dependency has no group: " + dependency.asString, e);
}
}
private ArtifactVersion parseVersion(ResolvedDependencyInfoImpl resolved) {
try {
return VersionRange.createFromVersionSpec(Objects.requireNonNull(resolved.version)).getRecommendedVersion();
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException("Version is invalid for: " + resolved.asString, e);
} catch (NullPointerException e) {
throw new IllegalArgumentException("Version is unspecified for: " + resolved.asString, e);
}
}
private VersionRange parseVersionRange(ResolvedDependencyInfoImpl dependency) {
if (dependency.hasManuallySpecifiedRange) {
try {
return VersionRange.createFromVersionSpec(dependency.versionRange);
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException("Version is invalid for: " + dependency.asString, e);
}
} else {
try {
return VersionRange.createFromVersionSpec("[%s,)".formatted(Objects.requireNonNull(dependency.versionRange)));
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException("Version range is invalid for: " + dependency.asString, e);
} catch (NullPointerException e) {
throw new IllegalArgumentException("Version is unspecified for: " + dependency.asString, e);
}
}
}
public interface ResolvedDependencyInfo {
void containedJarMetadata(Action<? super ContainedJarMetadataInfo> action);
void setResource(String resource);
void setLayer(String layer);
void setId(String identifier);
void setNested(boolean nested);
interface ContainedJarMetadataInfo {
void setGroup(String group);
void setName(String name);
void setPath(String version);
}
}
static final class ResolvedDependencyInfoImpl implements ResolvedDependencyInfo, ResolvedDependencyInfo.ContainedJarMetadataInfo, Serializable {
private static final @Serial long serialVersionUID = -7577318115877822993L;
final MinimalModuleVersionIdentifier module;
final String version;
String versionRange;
boolean hasManuallySpecifiedRange;
boolean nested;
final File artifact;
String path;
String resource;
String layer;
String identifier;
final String asString;
public ResolvedDependencyInfoImpl(MinimalModuleVersionIdentifier module, String version, File artifact, String asString) {
this.module = module;
this.version = version;
this.artifact = artifact;
this.asString = asString;
}
@Override
public void containedJarMetadata(Action<? super ContainedJarMetadataInfo> action) {
action.execute(this);
}
@Override
public void setGroup(String group) {
this.module.group = group;
}
@Override
public void setName(String name) {
this.module.name = name;
}
@Override
public void setPath(String path) {
this.path = path;
}
@Override
public void setResource(String resource) {
this.resource = resource;
}
@Override
public void setLayer(String layer) {
this.layer = layer;
}
@Override
public void setId(String identifier) {
this.identifier = identifier;
}
@Override
public void setNested(boolean nested) {
this.nested = nested;
}
static Set<File> getFiles(Set<ResolvedDependencyInfoImpl> resolvedDependencies) {
var ret = new HashSet<File>(resolvedDependencies.size());
for (var dependency : resolvedDependencies) {
ret.add(dependency.artifact);
}
return ret;
}
static ResolvedDependencyInfoImpl from(ConfigurationContainer configurations, Dependency dependency) {
var group = dependency.getGroup();
var name = dependency.getName();
var version = dependency.getVersion();
if (dependency instanceof FileCollectionDependency filesDependency) {
File artifact;
try {
artifact = filesDependency.getFiles().getSingleFile();
} catch (IllegalStateException e) {
// TODO fileCollectionDependencyIsNotSingleFile
throw e;
}
return new ResolvedDependencyInfoImpl(
new MinimalModuleVersionIdentifier(group, name, version),
version,
artifact,
filesDependency.toString()
);
} else if (dependency instanceof ModuleDependency moduleDependency) {
moduleDependency = moduleDependency.copy();
if (moduleDependency instanceof ExternalModuleDependency externalModuleDependency) {
externalModuleDependency.version(v -> v.strictly(version.toString()));
}
var detachedConfiguration = configurations.detachedConfiguration(moduleDependency);
detachedConfiguration.setTransitive(false);
ResolvedDependencyInfoImpl ret = null;
for (var artifact : detachedConfiguration.getResolvedConfiguration().getFirstLevelModuleDependencies().iterator().next().getModuleArtifacts()) {
var fileName = getFileName(artifact);
if (!fileName.endsWith(".jar"))
continue;
if (ret != null)
throw new IllegalArgumentException("Module dependency has too many Jar artifacts: " + moduleDependency);
ret = new ResolvedDependencyInfoImpl(
new MinimalModuleVersionIdentifier(group, name, artifact.getModuleVersion().getId().getVersion()),
version,
artifact.getFile(),
moduleDependency.toString()
);
}
if (ret == null)
throw new IllegalArgumentException("Module dependency has no Jar artifacts: " + moduleDependency);
return ret;
} else {
throw new IllegalArgumentException("Unsupported dependency type: " + dependency.getClass().getName() + " -- " + dependency);
}
}
private static String getFileName(ResolvedArtifact artifact) {
try {
return InvokerHelper.getProperty(artifact.getId(), "fileName").toString();
} catch (Throwable e) {
// NOTE: Why not just use this to begin with?
// ComponentArtifactIdentifier can have a getFileName() method, which doesn't necessarily resolve the file itself.
// This allows us to get the name of the file to be used without asking Gradle to download the file.
// So, if a file is not a JAR file, we can check the name without actually downloading it.
return artifact.getFile().getName();
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (ResolvedDependencyInfoImpl) obj;
return Objects.equals(this.module, that.module) &&
Objects.equals(this.version, that.version) &&
Objects.equals(this.versionRange, that.versionRange) &&
Objects.equals(this.artifact, that.artifact);
}
@Override
public int hashCode() {
return Objects.hash(module, version, versionRange, artifact);
}
@Override
public String toString() {
return "ResolvedDependencyInfo[" +
"module=" + module + ", " +
"fixedVersion=" + version + ", " +
"versionRange=" + versionRange + ", " +
"artifact=" + artifact + ']';
}
static final class MinimalModuleVersionIdentifier implements ModuleIdentifier, ModuleVersionIdentifier {
private static final @Serial long serialVersionUID = -955346236759069739L;
private String group;
private String name;
private final String version;
@Inject
public MinimalModuleVersionIdentifier(String group, String name, String version) {
this.group = group;
this.name = name;
this.version = version;
}
@Override
public ModuleIdentifier getModule() {
return this;
}
@Override
public String getGroup() {
return this.group;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getVersion() {
return this.version;
}
@Override
public boolean equals(Object obj) {
return this == obj || obj instanceof MinimalModuleVersionIdentifier o
&& Objects.equals(this.group, o.group)
&& Objects.equals(this.name, o.name)
&& Objects.equals(this.version, o.version);
}
@Override
public int hashCode() {
return Objects.hash(group, name, version);
}
@Override
public String toString() {
return "MinimalModuleVersionIdentifier[" +
"group=" + group + ", " +
"name=" + name + ", " +
"version=" + version + ']';
}
}
}
}
@@ -6,7 +6,6 @@ import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.nio.file.Files
@@ -17,78 +16,59 @@ abstract class LauncherJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input Map<String, Object> json = new LinkedHashMap<>()
@Input @Optional abstract SetProperty<String> getPackedDependencies()
@Internal final vanilla = project.project(':mcp').file('build/mcp/downloadJson/version.json')
@Internal final timestamp = iso8601Now()
@Internal final comment = [
"Please do not automate the download and installation of Forge.",
"Our efforts are supported by ads from the download page.",
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
]
@Internal final id = "${project.rootProject.ext.MC_VERSION}-${project.name}${project.version.substring(project.rootProject.ext.MC_VERSION.length())}"
LauncherJson() {
output.convention(project.layout.buildDirectory.file('libs/version.json'))
getOutput().convention(project.layout.buildDirectory.file('version.json'))
dependsOn(project.tasks.universalJar)
input.from(project.tasks.universalJar.archiveFile)
input.from(project.configurations.installer)
configure {
def mc = project.rootProject.ext.MC_VERSION
def forge = project.rootProject.ext.FORGE_VERSION
def timestamp = iso8601Now()
json.putAll([
_comment: [
"Please do not automate the download and installation of Forge.",
"Our efforts are supported by ads from the download page.",
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
],
id: "$mc-$project.name-$forge",
time: timestamp,
releaseTime: timestamp,
inheritsFrom: mc,
type: 'release',
logging: [:],
mainClass: '',
libraries: []
] as LinkedHashMap)
[
project.tasks.universalJar
].forEach { packed ->
dependsOn(packed)
input.from packed.archiveFile
dependsOn(':fmlloader:jar', 'universalJar')
getInput().from(project.tasks.universalJar.archiveFile,
project.project(':fmlloader').jar.archiveFile,
vanilla)
project.afterEvaluate {
packedDependencies.get().forEach {
def jarTask = project.rootProject.tasks.findByPath(it)
dependsOn(jarTask)
input.from jarTask.archiveFile
}
def patched = project.tasks.applyClientBinPatches
dependsOn(patched)
input.from patched.output
}
}
@TaskAction
protected void exec() {
var packed = (AbstractArchiveTask) project.tasks.universalJar
def info = Util.getMavenInfoFromTask(packed)
json.libraries.add([
name: info.name,
downloads: [
artifact: [
path: info.path,
url: "https://maven.minecraftforge.net/$info.path",
sha1: packed.archiveFile.get().asFile.sha1(),
size: packed.archiveFile.get().asFile.length()
if (!json.libraries)
json.libraries = []
def libs = [:]
getArtifacts(project, project.configurations.installer, false).each { key, lib -> libs[key] = lib }
getArtifacts(project, project.configurations.moduleonly, false).each { key, lib -> libs[key] = lib }
packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) }.forEach {
def path = Util.getMavenPath(it)
def key = Util.getMavenDep(it)
libs[key] = [
name: key,
downloads: [
artifact: [
path: path,
url: "https://maven.minecraftforge.net/${path}",
sha1: it.archiveFile.get().asFile.sha1(),
size: it.archiveFile.get().asFile.length()
]
]
]
])
var classifier = 'client'
var genned = project.tasks.applyClientBinPatches
info = Util.getMavenInfoFromTask(genned, classifier)
json.libraries.add([
name: info.name,
downloads: [
artifact: [
path: info.path,
url: "",
sha1: genned.output.get().asFile.sha1(),
size: genned.output.get().asFile.length()
]
]
])
json.libraries.addAll(getArtifacts(project.configurations.installer).values())
}
libs.each { key, lib -> json.libraries.add(lib) }
Files.writeString(output.get().asFile.toPath(), new JsonBuilder(json).toPrettyString())
}
}
@@ -1,6 +1,5 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import org.apache.commons.io.IOUtils
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
@@ -13,7 +12,6 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@CompileStatic
abstract class MergeJars extends DefaultTask {
MergeJars() {
output.convention(project.layout.buildDirectory.dir(name).map { it.file('output.jar') })
@@ -26,8 +24,8 @@ abstract class MergeJars extends DefaultTask {
try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output.get().asFile))) {
for (def jar : jars) {
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(jar))) {
ZipEntry entry
while ((entry = zin.getNextEntry()) !== null) {
def entry
while ((entry = zin.getNextEntry()) != null) {
ZipEntry _new = new ZipEntry(entry.getName())
_new.setTime(0) //SHOULD be the same time as the main entry, but NOOOO _new.setTime(entry.getTime()) throws DateTimeException, so you get 0, screw you!
zout.putNextEntry(_new)
@@ -27,6 +27,6 @@ public class ObjectTarget implements Comparable<ObjectTarget> {
@Override
int compareTo(ObjectTarget o) {
return toString() <=> o.toString()
return toString().compareTo(o.toString())
}
}
@@ -16,7 +16,7 @@ abstract class SetupCheckJarCompatibility extends DefaultTask {
SetupCheckJarCompatibility() {
group = 'jar compatibility'
onlyIf {
inputVersion.getOrNull() !== null
inputVersion.getOrNull() != null
}
outputs.upToDateWhen { false } // Never up to date, because this setup task should always run
@@ -26,6 +26,16 @@ abstract class SetupCheckJarCompatibility extends DefaultTask {
@TaskAction
void run() {
def inputVersion = inputVersion.get()
def fmlLibs = project.configurations.detachedConfiguration(project.PACKED_DEPS.collect {
def artifactId = it.split(':')[1]
return project.dependencies.create("net.minecraftforge:${artifactId}:${inputVersion}")
}.toArray(Dependency[]::new))
project.tasks.named('checkJarCompatibility') {
baseLibraries.from(project.provider {
fmlLibs.resolvedConfiguration.lenientConfiguration.files
})
}
def baseForgeUserdev = project.layout.buildDirectory.dir(name).map { it.file("forge-${inputVersion}-userdev.jar") }.get().asFile
project.rootProject.extensions.download.run {
@@ -32,7 +32,7 @@ class TeamcityRequests {
@Nullable
static Map<String, Build> buildsByCommit() throws IOException {
final Map<String, Build> builds = [:]
jsonRequest(new TypeToken<Builds>() {}, 'https://teamcity.minecraftforge.net/guestAuth/app/rest/builds?fields=build:(revisions,number)&locator=buildType:(id:MinecraftForge_MinecraftForge_MinecraftForge_MinecraftForge__Build),defaultFilter:false,count:300,status:SUCCESS')
jsonRequest(new TypeToken<Builds>() {}, "https://teamcity.minecraftforge.net/guestAuth/app/rest/builds?fields=build:(revisions,number)&locator=buildType:(id:MinecraftForge_MinecraftForge_MinecraftForge_MinecraftForge__Build),count:300,status:SUCCESS")
?.build?.forEach {
it.revisions.revision.each { rev -> builds[rev.version] = it }
}
@@ -2,224 +2,167 @@ package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.io.File
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Semaphore
import java.util.Date
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
final class Util {
public static final int ASM_LEVEL = Opcodes.ASM9
private static final HttpClient HTTP = HttpClient.newBuilder().build()
public class Util {
static final ASM_LEVEL = Opcodes.ASM9
static void init() {
File.metaClass.sha1 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-1')
delegate.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
static void init() {
File.metaClass.sha1 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-1')
delegate.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size)
}
return md.digest().collect {String.format "%02x", it}.join()
}
File.metaClass.getSha1 = { !delegate.exists() ? null : delegate.sha1() }
File.metaClass.sha256 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-256')
delegate.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
File.metaClass.getSha256 = { !delegate.exists() ? null : delegate.sha256() }
File.metaClass.json = { -> new JsonSlurper().parseText(delegate.text) }
File.metaClass.json = { -> new JsonSlurper().parseText(delegate.text) }
File.metaClass.getJson = { return delegate.exists() ? new JsonSlurper().parse(delegate) : [:] }
File.metaClass.setJson = { json -> delegate.text = new JsonBuilder(json).toPrettyString() }
Date.metaClass.iso8601 = { ->
var format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
var result = format.format(delegate)
return result[0..21] + ':' + result[22..-1]
}
Date.metaClass.iso8601 = { ->
def format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
def result = format.format(delegate)
return result[0..21] + ':' + result[22..-1]
}
String.metaClass.rsplit = { String del, int limit = -1 ->
var lst = new ArrayList<String>()
int x = 0
int idx
String tmp = delegate
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit === -1 || x++ < limit)) {
def lst = new ArrayList()
def x = 0, idx
def tmp = delegate
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit == -1 || x++ < limit)) {
lst.add(0, tmp.substring(idx + del.length(), tmp.length()))
tmp = tmp.substring(0, idx)
}
lst.add(0, tmp)
return lst
}
}
public static String[] getClasspath(project, libs, artifact) {
def ret = []
artifactTree(project, artifact).each { key, lib ->
libs[lib.name] = lib
if (lib.name != artifact)
ret.add(lib.name)
}
return ret
}
public static def getArtifacts(project, config, classifiers) {
def ret = [:]
config.resolvedConfiguration.resolvedArtifacts.each {
def art = [
group: it.moduleVersion.id.group,
name: it.moduleVersion.id.name,
version: it.moduleVersion.id.version,
classifier: it.classifier,
extension: it.extension,
file: it.file
]
def key = art.group + ':' + art.name
def folder = "${art.group.replace('.', '/')}/${art.name}/${art.version}/"
def filename = "${art.name}-${art.version}"
if (art.classifier != null)
filename += "-${art.classifier}"
filename += ".${art.extension}"
def path = "${folder}${filename}"
def url = "https://libraries.minecraft.net/${path}"
if (!checkExists(url)) {
url = "https://maven.minecraftforge.net/${path}"
}
//TODO remove when Mojang launcher is updated
if (!classifiers && art.classifier != null) {
//Mojang launcher doesn't currently support classifiers, so... move it to part of the version, and force the extension to 'jar'
// However, keep the path normal so that our mirror system works.
art.version = "${art.version}-${art.classifier}"
art.classifier = null
art.extension = 'jar'
}
ret[key] = [
name: "${art.group}:${art.name}:${art.version}" + (art.classifier == null ? '' : ":${art.classifier}") + (art.extension == 'jar' ? '' : "@${art.extension}"),
downloads: [
artifact: [
path: path,
url: url,
sha1: sha1(art.file),
size: art.file.length()
]
]
]
}
return ret
}
public static def getMavenPath(task) {
def classifier = task.archiveClassifier.get()
def dep = "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
return "${task.project.group.replace('.', '/')}/${task.project.name}/${task.project.version}/${task.project.name}-${task.project.version}".toString() + (classifier == '' ? '' : '-' + classifier) + '.jar'
}
static String[] getClasspath(Project project, Map libs, String artifact) {
def ret = []
artifactTree(project, artifact).each { key, lib ->
libs[lib.name] = lib
if (lib.name != artifact)
ret.add(lib.name)
}
return ret
public static def getMavenDep(task) {
def classifier = task.archiveClassifier.get()
return "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
}
@CompileStatic
static Map getArtifacts(Configuration config) {
var ret = [:]
var semaphore = new Semaphore(1, true)
config.resolvedConfiguration.resolvedArtifacts.parallelStream().forEachOrdered(dep -> {
var info = getMavenInfoFromDep(dep)
var domain = 'libraries.minecraft.net'
var url = "https://$domain/$info.path"
if (!checkExists(url))
url.values[0] = 'maven.minecraftforge.net'
public static def iso8601Now() { new Date().iso8601() }
var sha1 = sha1(dep.file)
public static def sha1(file) {
MessageDigest md = MessageDigest.getInstance('SHA-1')
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size)
}
return md.digest().collect {String.format "%02x", it}.join()
}
semaphore.acquire()
ret[info.key] = [
name: info.name,
downloads: [
artifact: [
path: info.path,
url: url.toString(),
sha1: sha1,
size: dep.file.length()
]
]
]
semaphore.release()
})
return ret
}
private static def artifactTree(project, artifact) {
if (!project.ext.has('tree_resolver'))
project.ext.tree_resolver = 1
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
def dep = project.dependencies.create(artifact)
cfg.dependencies.add(dep)
def files = cfg.resolve()
return getArtifacts(project, cfg, true)
}
@CompileStatic
static Map getMavenInfoFromDep(ResolvedArtifact dep) {
return getMavenInfoFromMap([
group: dep.moduleVersion.id.group,
name: dep.moduleVersion.id.name,
version: dep.moduleVersion.id.version,
classifier: dep.classifier,
extension: dep.extension
])
}
private static boolean checkExists(url) {
try {
def code = new URL(url).openConnection().with {
requestMethod = 'HEAD'
connect()
responseCode
}
return code == 200
} catch (Exception e) {
if (e.toString().contains('unable to find valid certification path to requested target'))
throw new RuntimeException('Failed to connect to ' + url + ': Missing certificate root authority, try updating java')
throw e
}
}
@CompileStatic
static Map getMavenInfoFromTask(AbstractArchiveTask task) {
return getMavenInfoFromMap([
group: task.project.group.toString(),
name: task.project.name,
version: task.project.version.toString(),
classifier: task.archiveClassifier.get(),
extension: task.archiveExtension.get()
])
}
@CompileStatic
static Map getMavenInfoFromTask(Task task, String classifier) {
return getMavenInfoFromMap([
group: task.project.group.toString(),
name: task.project.name,
version: task.project.version.toString(),
classifier: classifier,
extension: 'jar'
])
}
@CompileStatic
private static Map getMavenInfoFromMap(Map<String, String> art) {
var key = "$art.group:$art.name"
var name = "$art.group:$art.name:$art.version"
var path = "${art.group.replace('.', '/')}/$art.name/$art.version/$art.name-$art.version"
if (art.classifier !== null) {
name += ":$art.classifier"
path += "-$art.classifier"
}
if ('jar' != art.extension) {
name += "@$art.extension"
path += ".$art.extension"
} else {
path += ".jar"
}
return [
key: key.toString(),
name: name.toString(),
path: path.toString(),
art: art
]
}
static String iso8601Now() { new Date().iso8601() }
@CompileStatic
static String sha1(File file) {
MessageDigest md = MessageDigest.getInstance('SHA-1')
file.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
@CompileStatic
private static String toHex(byte bite) {
return String.format('%02x', bite)
}
private static Map artifactTree(Project project, String artifact, boolean transitive = true) {
if (!project.ext.has('tree_resolver'))
project.ext.tree_resolver = 1
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
cfg.transitive = transitive
def dep = project.dependencies.create(artifact)
cfg.dependencies.add(dep)
def files = cfg.resolve()
return getArtifacts(cfg)
}
@CompileStatic
static boolean checkExists(String url) {
try {
return HTTP.send(HttpRequest.newBuilder(new URI(url))
.method('HEAD', HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.discarding()
).statusCode() === 200
} catch (Exception e) {
if (e.toString().contains('unable to find valid certification path to requested target'))
throw new RuntimeException("Failed to connect to $url: Missing certificate root authority, try updating Java")
throw e
}
}
static String getLatestForgeVersion(String mcVersion) {
static String getLatestForgeVersion(mcVersion) {
final json = new JsonSlurper().parseText(new URL('https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json').getText('UTF-8'))
final ver = json.promos["$mcVersion-latest"]
ver === null ? null : (mcVersion + '-' + ver)
}
@CompileStatic
static void processClassNodes(File file, @ClosureParams(value = SimpleType, options = 'org.objectweb.asm.tree.ClassNode') Closure process) {
static void processClassNodes(File file, Closure process) {
file.withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
ZipEntry zein
while ((zein = zin.nextEntry) !== null) {
while ((zein = zin.nextEntry) != null) {
if (zein.name.endsWith('.class')) {
var node = new ClassNode(ASM_LEVEL)
def node = new ClassNode(ASM_LEVEL)
new ClassReader(zin).accept(node, 0)
process(node)
}
@@ -227,4 +170,4 @@ final class Util {
}
}
}
}
}
@@ -5,7 +5,6 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import net.minecraftforge.srgutils.MinecraftVersion
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
@@ -29,8 +28,8 @@ abstract class ValidateDeprecations extends DefaultTask {
@TaskAction
protected void exec() {
var mcVer = MinecraftVersion.from(mcVersion.get())
List<String> errors = []
def mcVer = MinecraftVersion.from(mcVersion.get())
def errors = []
Util.processClassNodes(input.get().asFile) {
processNode(mcVer, errors, it)
@@ -66,7 +65,7 @@ abstract class ValidateDeprecations extends DefaultTask {
}
}
private static void processAnnotations(AnnotationNode annotation, MinecraftVersion mcVer, List<String> errors, Closure<String> context) {
private static void processAnnotations(AnnotationNode annotation, MinecraftVersion mcVer, List<String> errors, Closure context) {
def values = annotation.values
if (values === null)
return
@@ -74,7 +73,7 @@ abstract class ValidateDeprecations extends DefaultTask {
int since = values.indexOf('since')
if (annotation.desc == 'Ljava/lang/Deprecated;' && forRemoval !== -1 && since !== -1 && values.size() >= 4 && values[forRemoval + 1] === true) {
def oldVersion = MinecraftVersion.from(values[since + 1])
int[] split = ValidateDeprecations.splitDots(oldVersion.toString())
def split = ValidateDeprecations.splitDots(oldVersion.toString())
if (split.length < 2)
return
def removeVersion = MinecraftVersion.from("${split[0]}.${split[1] + 1}")
@@ -83,7 +82,6 @@ abstract class ValidateDeprecations extends DefaultTask {
}
}
@CompileStatic
private static int[] splitDots(String version) {
String[] pts = version.split('\\.')
int[] values = new int[pts.length]
@@ -17,15 +17,17 @@ abstract class CheckATs extends CheckTask {
@InputFile abstract RegularFileProperty getInheritance()
@InputFiles abstract ConfigurableFileCollection getAts()
@InputFile @Optional abstract RegularFileProperty getMappings()
@Override
void check(Reporter reporter, boolean fix) {
final IMappingFile mappings = mappings.map { RegularFile it -> IMappingFile.load(it.asFile) }.getOrNull()
final inheritance = InheritanceData.parse(this.inheritance.get().asFile)
ats.each {
final lines = process(it, reporter, inheritance)
if (fix) {
it.text = joinBack(lines, inheritance).join('\n')
it.text = joinBack(lines, inheritance, mappings).join('\n')
}
}
}
@@ -168,18 +170,45 @@ abstract class CheckATs extends CheckTask {
return lines
}
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance) {
final data = [] as List<String>
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance, IMappingFile mappings) {
final data = []
final remapComment = { ATParser.Entry entry ->
if (!mappings || !entry || !entry.desc) return null
final comment = entry.comment?.substring(1)?.trim()
final jsonCls = inheritance.get(entry.cls.replaceAll('\\.', '/'))
final mappingsClass = mappings?.getClass(jsonCls.name)
if (mappingsClass === null) return entry.comment
final idx = entry.desc.indexOf('(')
String mappedName = idx == -1
? mappingsClass.remapField(entry.desc)
: mappingsClass.remapMethod(entry.desc.substring(0, idx), entry.desc.substring(idx))
if (!mappedName) return entry.comment
if (mappedName == '<init>')
mappedName = 'constructor'
if (comment?.startsWith(mappedName))
return '# ' + comment
if (comment && comment.indexOf(' ') !== -1) {
def split = comment.split(' - ').toList()
if (split[0].indexOf(' ') !== -1)
// The first string is more than one word, so append before it
return "# ${mappedName} - ${comment}"
split.remove(0)
return "# ${mappedName} - ${String.join(' - ', split)}"
}
return '# ' + mappedName
}
lines.each { key, value ->
if (!value.group) {
def comment = null //value.comment
def comment = remapComment.call(value)
data.add(value.modifier + ' ' + key + (comment ? ' ' + comment : ''))
} else {
data.add(('#group ' + value.modifier + ' ' + key + ' ' + (value.comment ?: '')).trim())
value.children.each {
final line = value.modifier + ' ' + it
final entry = ATParser.parseEntry(line)
final comment = null //entry.comment
final comment = remapComment(entry)
data.add(line + (comment ? ' ' + comment : ''))
}
data.add('#endgroup')
@@ -260,7 +289,6 @@ class ATParser {
final String comment = idx === -1 ? null : line.substring(idx)
if (idx !== -1) line = line.substring(0, idx - 1)
final data = (line.trim() + ' ').split(' ', -1)
data[1] = data[1].replaceAll('/', '.') // Convert to Source names, internal names are fine by spec, but not supported by old AST based AT implementations.
new Entry(data[0], data[1], data[2], comment)
}
@@ -26,12 +26,12 @@ abstract class CheckExcs extends CheckTask {
excs.each { f ->
final lines = []
f.eachLine { line ->
int idx = line.indexOf('#')
if (idx === 0 || line.isEmpty()) {
def idx = line.indexOf('#')
if (idx == 0 || line.isEmpty()) {
return
}
if (idx !== -1) line = line.substring(0, idx - 1)
if (idx != -1) line = line.substring(0, idx - 1)
if (!line.contains('=')) {
reporter.report("Invalid: $line")
@@ -58,8 +58,6 @@ abstract class CheckExcs extends CheckTask {
return
}
lines.add(line)
return
}
if (fix) f.text = lines.sort().join('\n')
@@ -6,6 +6,4 @@ import groovy.transform.CompileStatic
enum CheckMode {
CHECK,
FIX
CheckMode() {}
}
@@ -36,13 +36,13 @@ abstract class CheckPatches extends CheckTask {
}
}
static boolean accessChange(String previous, String current) {
def accessChange(previous, current) {
//return ACCESS_MAP[previous] < ACCESS_MAP[current]
return previous != current
}
void verifyPatch(Path patch, Reporter reporter, boolean fix, String patchPath, boolean hasS2SArtifact) {
int oldFixedErrors = reporter.fixed.size()
final oldFixedErrors = reporter.fixed.size()
final lines = Files.readAllLines(patch)
@@ -140,7 +140,7 @@ abstract class CheckPatches extends CheckTask {
final nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-')
if (!prevLineChange && !nextLineChange) {
reporter.report("Patch contains white space change in valid hunk at line ${i + 1}, file: $patchPath \n$prevLine\n$line\n${lines[i+1]}", false)
reporter.report("Patch contains white space change in valid hunk at line ${i + 1}, file: $patchPath", false)
}
}
@@ -94,7 +94,7 @@ abstract class CheckSAS extends CheckTask {
}
}
protected static boolean isSided(Annotatable annotatable) {
protected static isSided(Annotatable annotatable) {
if (annotatable === null) return false
for (ann in annotatable.annotations) {
if ('Lnet/minecraftforge/api/distmarker/OnlyIn;' == ann.desc)
@@ -104,7 +104,7 @@ abstract class CheckSAS extends CheckTask {
}
protected static findChildMethods(Map<String, InheritanceData> json, String cls, String desc) {
return json.values().findAll{ it.methods !== null && it.methods[desc] !== null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
return json.values().findAll{ it.methods != null && it.methods[desc] != null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
.collect { it.name + ' ' + desc.replace(' ', '') } as TreeSet
}
}
@@ -1,8 +1,6 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.ThirdParam
import net.minecraftforge.forge.tasks.Util
import org.gradle.api.Action
import org.gradle.api.DefaultTask
@@ -35,10 +33,10 @@ abstract class CheckTask extends DefaultTask implements VerificationTask {
void run() {
Util.init()
boolean doFix = getMode().get() === CheckMode.FIX
final doFix = getMode().get() === CheckMode.FIX
final Reporter reporter = new Reporter(doFix)
check(reporter, doFix)
if (reporter.messages) {
if (getMode().get() === CheckMode.CHECK) {
logger.error("Check task '{}' found errors:\n{}", name, reporter.messages.join('\n'))
@@ -90,24 +88,21 @@ abstract class CheckTask extends DefaultTask implements VerificationTask {
}
static <T extends CheckTask> void registerTask(TaskContainer tasks, String taskName, @DelegatesTo.Target('type') Class<T> clazz,
@DelegatesTo(genericTypeIndex = 0, target = 'type')
@ClosureParams(ThirdParam.FirstGenericType) Closure configuration) {
@DelegatesTo(genericTypeIndex = 0, target = 'type') Closure configuration) {
taskName = taskName.capitalize()
tasks.register("check$taskName", clazz) { CheckTask task ->
def castedTask = task as T
configuration.setDelegate(castedTask)
configuration.call(castedTask)
castedTask.mode.set(CheckMode.CHECK)
castedTask.group = 'checks'
configuration.setDelegate((T) task)
configuration.call(task)
task.mode.set(CheckMode.CHECK)
task.group = 'checks'
}
tasks.named('checkAll').configure { it.dependsOn("check$taskName") }
tasks.register("checkAndFix$taskName", clazz) { CheckTask task ->
def castedTask = task as T
configuration.setDelegate(castedTask)
configuration.call(castedTask)
castedTask.mode.set(CheckMode.FIX)
castedTask.group = 'checks'
configuration.setDelegate((T) task)
configuration.call(task)
task.mode.set(CheckMode.FIX)
task.group = 'checks'
}
tasks.named('checkAllAndFix').configure { it.dependsOn("checkAndFix$taskName") }
}
-73
View File
@@ -1,73 +0,0 @@
plugins {
id 'java-library'
id 'net.minecraftforge.gradle.patcher'
}
evaluationDependsOn(':mcp')
repositories {
mavenCentral()
maven gradleutils.forgeMaven
maven gradleutils.minecraftLibsMaven
}
java.toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
dependencies {
implementation(libs.forgespi)
}
patcher {
parent = project(':mcp')
mcVersion = MC_VERSION
patchedSrc = file('src/main/java')
mappings channel: MAPPING_CHANNEL, version: MAPPING_VERSION
runs {
clean_client {
client true
taskName 'clean_client'
ideaModule "${rootProject.name}.${project.name}.main"
main 'net.minecraft.client.main.Main'
workingDirectory project.file('run/client')
args '--gameDir', '.'
args '--version', MC_VERSION
args '--assetsDir', downloadAssets.output
args '--assetIndex', '{asset_index}'
args '--accessToken', '0'
}
clean_server {
client false
taskName 'clean_server'
ideaModule "${rootProject.name}.${project.name}.main"
main 'net.minecraft.server.Main'
workingDirectory project.file('run/server')
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
options.warnings = false // Shutup deprecated for removal warnings
}
eclipse.classpath.file.whenMerged {
// Disable optional warnings on the minecraft decompiled source code. It just muddies the warning window and hides warnings in our own codebase
def src = entries.find { it.path == 'src/main/java' }
if (src == null) throw new IllegalStateException("You must run `setup` task before importing")
src.entryAttributes['ignore_optional_problems'] = 'true'
}
tasks.named('genPatches').configure {
onlyIf { false }
}
tasks.named('extractRangeMap').configure {
onlyIf { false }
tool = 'net.minecraftforge:Srg2Source:8.1.0:fatjar'
}
-1021
View File
File diff suppressed because it is too large. Load diff
-89
View File
@@ -1,89 +0,0 @@
import net.minecraftforge.forge.tasks.CleanProperties
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'net.minecraftforge.gradleutils'
group = 'net.minecraftforge'
version = VERSION
println("Version: $version")
repositories {
mavenCentral()
maven gradleutils.forgeMaven
maven gradleutils.minecraftLibsMaven
//mavenLocal()
}
tasks.withType(Javadoc).configureEach {
options.tags = [
'apiNote:a:<em>API Note:</em>',
'implSpec:a:<em>Implementation Requirements:</em>',
'implNote:a:<em>Implementation Note:</em>'
]
options.addStringOption('Xdoclint:all,-missing', '-public')
}
// We need to write the manifest to the binary file so we have properly versioned packaged at dev time.
tasks.register('writeManifest') {
doLast {
if (plugins.findPlugin('net.minecraftforge.gradle.patcher')) // Forge project
universalJar.manifest.writeTo(rootProject.file('src/main/resources/META-INF/MANIFEST.MF'))
else
jar.manifest.writeTo(project.file('src/main/resources/META-INF/MANIFEST.MF'))
}
}
tasks.register('generateResources') {
dependsOn('writeManifest')
}
tasks.named('processResources') {
dependsOn(generateResources)
}
// Make sure out manifests get written before compiling the code, IDEA calls this task if you tell it to use the gradle build.
tasks.withType(JavaCompile).configureEach {
dependsOn 'generateResources'
dependsOn 'processResources' // Needed because we merge the output of this with the output of the compile task. And gradle detects downstream tasks using the output without a hard dep
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
options.warnings = false // Shutup deprecated for removal warnings
options.forkOptions.jvmArgs += '-Xmx6G' // Needed to make compiling faster, and not run out of heap space in some cases.
}
// Merge the resources and classes into the same directory. We'll need to split them at runtime because
// Minecraft and Forge are in the same sourceSet as they are inter dependent.. for now..
sourceSets.each {
def dir = layout.buildDirectory.dir("classes/java/$it.name")
it.output.resourcesDir = dir
it.java.destinationDirectory = dir
}
tasks.register('copyEclipseSettings') {
doLast {
rootProject.fileTree('ide/eclipse/template/.settings/').matching { include '**/*.prefs' }.each { file ->
def target = project.file('.settings/' + file.name)
def temp = new CleanProperties().load(file)
def exst = new CleanProperties().load(target)
exst.put('eclipse.preferences.version', '1')
exst.putAll(temp)
exst.store(target)
}
}
}
// TODO: [Gradle][IntelliJ] Auto trigger these tasks on import.
eclipse {
// Run everytime eclipse builds the code
//autoBuildTasks writeManifest
// Run when importing the project
synchronizationTasks generateResources, copyEclipseSettings, eclipseClasspath, eclipseProject
}
idea {
module {
// IntelliJ IDEA does not do this by itself anymore...
downloadJavadoc = true
downloadSources = true
}
}
+7 -33
View File
@@ -2,15 +2,17 @@
MinecraftForge
=============
[![Stable Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Stable&prefix=1.21.11-&query=$.promos["1.21.11-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.1-&query=$.promos["26.1.1-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=1.19.4-&query=$.promos["1.19.4-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=1.19.4-&query=$.promos["1.19.4-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!
| Version | Support |
|--------------|-----------------------------|
| All versions | [Supported][Support-Policy] |
| Version | Support |
|---------| ------------- |
| 1.20.x | Active |
| 1.19.4 | Legacy |
| 1.19.2 | Legacy |
* [Download]
* [Forum]
@@ -30,20 +32,6 @@ You can download the installer for the *Recommended Build* or the
For support and questions, visit [the Support Forum][Support-Forum] or [the Forge Discord server][Discord].
## Required Java Versions
We require the version of Java that Minecraft requires, which is currently:
| Minecraft | Java |
|------------------|-------------------|
| 26.1+ | [25][Adoptium-25] |
| 1.20.6 - 1.21.11 | [21][Adoptium-21] |
| 1.18 - 1.20.4 | [17][Adoptium-17] |
| 1.17.1 | [16][Adoptium-16] |
| 1.16.5- | [8][Adoptium-8] |
**If you are on Windows and have difficulty using Java to launch JAR files, consider using [Jarfix by Johann Löfflmann][Jarfix].**
# Creating Mods
[See the "Getting Started" section in the Forge Documentation][Getting-Started].
@@ -71,26 +59,13 @@ Please ensure you have a valid email address associated with your GitHub account
to use and modify. However, it costs money to run such a large project as this, so please consider
[becoming a patron][Patreon].*
### 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.
[Contributing]: ./CONTRIBUTING.md
[CLA]: https://cla-assistant.io/MinecraftForge/MinecraftForge
[Download]: https://files.minecraftforge.net/
[Adoptium-25]: https://adoptium.net/temurin/releases/?version=25
[Adoptium-21]: https://adoptium.net/temurin/releases/?version=21
[Adoptium-17]: https://adoptium.net/temurin/releases/?version=17
[Adoptium-16]: https://adoptium.net/temurin/releases/?version=16
[Adoptium-8]: https://adoptium.net/temurin/releases/?version=8
[Jarfix]: https://johann.loefflmann.net/en/software/jarfix/index.html
[Forum]: https://forums.minecraftforge.net/
[Support-Forum]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
[Support-Policy]: https://forums.minecraftforge.net/topic/144690-new-tiered-support-policy
[Discord]: https://discord.minecraftforge.net/
@@ -100,4 +75,3 @@ YourKit for providing us accesss to their [YourKit Java Profiler] which helps id
[Pull-Requests]: https://docs.minecraftforge.net/en/latest/forgedev/#making-changes-and-pull-requests
[Patreon]: https://www.patreon.com/LexManos
[YourKit Java Profiler]: https://www.yourkit.com/java/profiler/
-24
View File
@@ -1,24 +0,0 @@
#!/usr/bin/env bash
for file in $(find repo | grep --invert-match -e md5 -e sha | grep -e .jar -e .zip); do
file_path=$(realpath $file)
file_name=$(basename $file)
echo "$file_name"
files=$(unzip -l $file | awk '{print $4}')
for f in $files; do
out="_actual/$file_name/$f"
parent=$(dirname "$out")
echo $out
if [ ! -d "$parent" ]; then
mkdir -p "$parent"
fi
if [[ $f == *.class ]]; then
javap -v -p jar:file://$file_path!/$f | tail -n +4 >$out.txt
elif [[ $f != */ && $f != "Name" && $f != "----" ]]; then
unzip -o -q $file $f -d _actual/$file_name
fi
done
done
+48 -43
View File
@@ -1,68 +1,73 @@
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
id 'com.github.ben-manes.versions'
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
id 'org.cadixdev.licenser'
}
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply plugin: 'org.javamodularity.moduleplugin'
apply from: rootProject.file('build_shared.gradle')
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
dependencies {
compileOnly(libs.jetbrains.annotations)
compileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
api(libs.eventbus)
annotationProcessor(libs.eventbus.validator)
api("net.minecraftforge:eventbus:${EVENTBUS_VERSION}")
implementation(project(':fmlloader'))
implementation(libs.commons.io)
implementation("commons-io:commons-io:${COMMONS_IO_VERSION}")
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
'FMLModType': 'LIBRARY'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FML',
ext {
MANIFESTS = [
'': [
'FMLModType': 'LIBRARY',
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
'Git-Commit': GIT_INFO.abbreviatedId,
'Git-Branch': GIT_INFO.branch,
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}"
] as LinkedHashMap,
'net/minecraftforge/fml/loading/': [
'Specification-Title': 'FMLLoader',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML',
'Implementation-Title': 'FML Loader',
'Implementation-Version': '1.0',
'Implementation-Vendor': 'Forge Development LLC'
] as LinkedHashMap, 'net/minecraftforge/fml/')
'Implementation-Vendor': 'Forge'
] as LinkedHashMap
]
}
jar.doFirst {
MANIFESTS.each { pkg, values ->
if (pkg == '')
manifest.attributes(values)
else
manifest.attributes(values, pkg)
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:-unchecked'
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
include 'net/minecraftforge/'
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlcore'
pom {
name = project.name
description = 'Modifactions to Minecraft to enable mod developers.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
artifacts {
archives jar
archives sourcesJar
}
publishing.publications.mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
}
@@ -5,39 +5,36 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.config.IConfigEvent;
import net.minecraftforge.fml.loading.FMLLoader;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.function.Supplier;
/**
* Used to allow access to certain things from the game layer
*/
public class Bindings {
private Bindings() {}
private static final Bindings INSTANCE = new Bindings();
/**
* The provider of the bindings
* @implNote May also throw a {@link ServiceConfigurationError}
*/
private static final IBindingsProvider PROVIDER = ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class)
.findFirst().orElseThrow(() -> new IllegalStateException("Could not find bindings provider"));
private final IBindingsProvider provider;
/**
* @return A supplier of net.minecraftforge.common.MinecraftForge#EVENT_BUS
*/
public static Supplier<BusGroup> getForgeBus() {
return PROVIDER.getForgeBusSupplier();
private Bindings() {
final var providers = ServiceLoaderUtils.streamServiceLoader(()->ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class), sce->{}).toList();
if (providers.size() != 1) {
throw new IllegalStateException("Could not find bindings provider");
}
this.provider = providers.get(0);
}
public static Supplier<IEventBus> getForgeBus() {
return INSTANCE.provider.getForgeBusSupplier();
}
public static Supplier<I18NParser> getMessageParser() {
return PROVIDER.getMessageParser();
return INSTANCE.provider.getMessageParser();
}
public static Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration() {
return PROVIDER.getConfigConfiguration();
return INSTANCE.provider.getConfigConfiguration();
}
}
@@ -57,9 +57,7 @@ public class DeferredWorkQueue
LOGGER.debug(LOADING, "Dispatching synchronous work for work queue {}: {} jobs", modLoadingStage, tasks.size());
RuntimeException aggregate = new RuntimeException();
Stopwatch timer = Stopwatch.createStarted();
for (TaskInfo t : tasks) {
makeRunnable(t, Runnable::run, aggregate);
}
tasks.forEach(t -> makeRunnable(t, Runnable::run, aggregate));
timer.stop();
if (aggregate.getSuppressed().length > 0) {
LOGGER.fatal(
@@ -68,7 +66,7 @@ public class DeferredWorkQueue
timer,
aggregate
);
throw aggregate;
//TODO 1.20: throw aggregate exception
} else {
LOGGER.debug(LOADING, "Synchronous work queue completed in {}", timer);
}
@@ -116,7 +114,7 @@ public class DeferredWorkQueue
return future;
}
private static final class TaskInfo {
private static class TaskInfo {
private final ModContainer owner;
private Runnable task;
private CompletableFuture<?> future;
@@ -0,0 +1,262 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.loading.FMLEnvironment;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
/**
* Use to execute code conditionally based on sidedness.
* <ul>
* <li>When you want to call something on one side and return a result {@link #safeCallWhenOn(Dist, Supplier)}</li>
* <li>When you want to call one thing on one side, another thing on the other and return a result {@link #safeRunForDist(Supplier, Supplier)}</li>
* <li>When you want to run something on one side {@link #safeRunWhenOn(Dist, Supplier)}</li>
* </ul>
*/
public final class DistExecutor
{
private static final Logger LOGGER = LogManager.getLogger();
private DistExecutor() {}
/**
* Run the callable in the supplier only on the specified {@link Dist}.
* This method is NOT sided-safe and special care needs to be taken in code using this method that implicit class
* loading is not triggered by the Callable.
*
* This method can cause unexpected ClassNotFound exceptions.
*
* Use {@link #safeCallWhenOn(Dist, Supplier)} where possible.
*
* @param dist The dist to run on
* @param toRun A supplier of the callable to run (Supplier wrapper to ensure classloading only on the appropriate dist)
* @param <T> The return type from the callable
* @return The callable's result
* @deprecated use {@link #safeCallWhenOn(Dist, Supplier)} instead. This remains for advanced use cases.
*/
@Deprecated
public static <T> T callWhenOn(Dist dist, Supplier<Callable<T>> toRun) {
return unsafeCallWhenOn(dist, toRun);
}
public static <T> T unsafeCallWhenOn(Dist dist, Supplier<Callable<T>> toRun) {
if (dist == FMLEnvironment.dist) {
try
{
return toRun.get().call();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
return null;
}
/**
* Call the SafeCallable when on the correct {@link Dist}.
*
* <strong>The lambda supplied here is required to be a method reference to a method defined in
* another class, otherwise an invalid SafeReferent error will be thrown</strong>
* @param dist the dist which this will run on
* @param toRun the SafeCallable to run and return the result from
* @param <T> The type of the SafeCallable
* @return the result of the SafeCallable or null if on the wrong side
*/
public static <T> T safeCallWhenOn(Dist dist, Supplier<SafeCallable<T>> toRun) {
validateSafeReferent(toRun);
return callWhenOn(dist, toRun::get);
}
/**
* Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #callWhenOn(Dist, Supplier)}.
*
* This method can cause unexpected ClassNotFound exceptions.
*
* @see #callWhenOn(Dist, Supplier)
* @param dist Dist to run this code on
* @param toRun The code to run
* @deprecated use {@link #safeRunWhenOn(Dist, Supplier)} where possible. Advanced uses only.
*/
@Deprecated
public static void runWhenOn(Dist dist, Supplier<Runnable> toRun) {
unsafeRunWhenOn(dist, toRun);
}
/**
* Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #unsafeCallWhenOn(Dist, Supplier)}.
*
* This method can cause unexpected ClassNotFoundException problems in common scenarios. Understand the pitfalls of
* the way the class verifier works to load classes before using this.
*
* Use {@link #safeRunWhenOn(Dist, Supplier)} if you can.
*
* @see #unsafeCallWhenOn(Dist, Supplier)
* @param dist Dist to run this code on
* @param toRun The code to run
*/
public static void unsafeRunWhenOn(Dist dist, Supplier<Runnable> toRun) {
if (dist == FMLEnvironment.dist) {
toRun.get().run();
}
}
/**
* Call the supplied SafeRunnable when on the correct Dist.
* @param dist The dist to run on
* @param toRun The code to run
*/
public static void safeRunWhenOn(Dist dist, Supplier<SafeRunnable> toRun) {
validateSafeReferent(toRun);
if (dist == FMLEnvironment.dist) {
toRun.get().run();
}
}
/**
* Executes one of the two suppliers, based on which side is active.
*
* <p>
* Example (replacement for old SidedProxy):<br/>
* {@code Proxy p = DistExecutor.runForDist(()->ClientProxy::new, ()->ServerProxy::new);}
*
* NOTE: the double supplier is required to avoid classloading the secondary target.
*
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
* @param <T> The common type to return
* @return The returned instance
* @deprecated Use {@link #safeRunForDist(Supplier, Supplier)}
*/
@Deprecated
public static <T> T runForDist(Supplier<Supplier<T>> clientTarget, Supplier<Supplier<T>> serverTarget) {
return unsafeRunForDist(clientTarget, serverTarget);
}
/**
* Unsafe version of {@link #safeRunForDist(Supplier, Supplier)}. Use only when you know what you're doing
* and understand why the verifier can cause unexpected ClassNotFoundException crashes even when code is apparently
* not sided. Ensure you test both sides fully to be confident in using this.
*
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
* @param <T> The common type to return
* @return The returned instance
*/
public static <T> T unsafeRunForDist(Supplier<Supplier<T>> clientTarget, Supplier<Supplier<T>> serverTarget) {
switch (FMLEnvironment.dist)
{
case CLIENT:
return clientTarget.get().get();
case DEDICATED_SERVER:
return serverTarget.get().get();
default:
throw new IllegalArgumentException("UNSIDED?");
}
}
/**
* Executes one of the two suppliers, based on which side is active.
*
* <p>
* Example (replacement for old SidedProxy):<br/>
* {@code Proxy p = DistExecutor.safeRunForDist(()->ClientProxy::new, ()->ServerProxy::new);}
*
* NOTE: the double supplier is required to avoid classloading the secondary target.
*
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
* @param <T> The common type to return
* @return The returned instance
*/
public static <T> T safeRunForDist(Supplier<SafeSupplier<T>> clientTarget, Supplier<SafeSupplier<T>> serverTarget) {
validateSafeReferent(clientTarget);
validateSafeReferent(serverTarget);
switch (FMLEnvironment.dist)
{
case CLIENT:
return clientTarget.get().get();
case DEDICATED_SERVER:
return serverTarget.get().get();
default:
throw new IllegalArgumentException("UNSIDED?");
}
}
/**
* A safe referent. This will assert that it is being called via a separated class method reference. This will
* avoid the common pitfalls of {@link #callWhenOn(Dist, Supplier)} above.
*
* SafeReferents assert that they are defined as a separate method outside the scope of the calling class.
*
* <strong>Implementations need to be defined in a separate class to the referring site, with appropriate
* visibility to be accessible at the callsite (generally, avoid private methods).</strong>
*
* <p>
* Valid:<br/>
*
* {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->AnotherClass::clientOnlyMethod);}
*
* <p>
* Invalid:<br/>
*
* {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->()->Minecraft.getInstance().world);}
*/
public interface SafeReferent {}
/**
* SafeCallable version of {@link SafeReferent}.
* @see SafeReferent
* @param <T> The return type of the Callable
*/
public interface SafeCallable<T> extends SafeReferent, Callable<T>, Serializable {}
/**
* SafeSupplier version of {@link SafeReferent}
* @param <T> The return type of the Supplier
*/
public interface SafeSupplier<T> extends SafeReferent, Supplier<T>, Serializable {}
/**
* SafeRunnable version of {@link SafeReferent}
*/
public interface SafeRunnable extends SafeReferent, Runnable, Serializable {}
private static final void validateSafeReferent(Supplier<? extends SafeReferent> safeReferentSupplier) {
if (FMLEnvironment.production) return;
final SafeReferent setter;
try {
setter = safeReferentSupplier.get();
} catch (Exception e) {
// Typically a class cast exception, just return out, expected.
return;
}
for (Class<?> cl = setter.getClass(); cl != null; cl = cl.getSuperclass()) {
try {
Method m = cl.getDeclaredMethod("writeReplace");
m.setAccessible(true);
Object replacement = m.invoke(setter);
if (!(replacement instanceof SerializedLambda))
break;// custom interface implementation
SerializedLambda l = (SerializedLambda) replacement;
if (Objects.equals(l.getCapturingClass(), l.getImplClass())) {
LOGGER.fatal("Detected unsafe referent usage, please view the code at {}",Thread.currentThread().getStackTrace()[3]);
throw new RuntimeException("Unsafe Referent usage found in safe referent method");
}
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException | InvocationTargetException e) {
break;
}
}
}
}
@@ -5,13 +5,13 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.config.IConfigEvent;
import java.util.function.Supplier;
public interface IBindingsProvider {
Supplier<BusGroup> getForgeBusSupplier();
Supplier<IEventBus> getForgeBusSupplier();
Supplier<I18NParser> getMessageParser();
Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration();
}
@@ -25,7 +25,6 @@ import java.util.function.Supplier;
* @param <T> the type of the record which is held by the extension point
*/
@SuppressWarnings("unused") // Type parameter T
// TODO: Complete re-write, this hasnt actually be used in a generic way by mods and we could make it a lot simpler.
public interface IExtensionPoint<T extends Record>
{
/**
@@ -41,7 +40,7 @@ public interface IExtensionPoint<T extends Record>
* where {@code true} means it is from the server and {@code false} means it is from the world save. The return
* value of the predicate determines whether the remote version is "compatible" for the purposes of the display test.</p>
*
* <p>The local compatibility version may be of the value {@link DisplayTest#IGNORESERVERONLY},
* <p>The local compatibility version may be of the value {@link net.minecraftforge.network.NetworkConstants#IGNORESERVERONLY},
* in which case clients will ignore the mod's presence if it is present on the server but not on the client.
* However, the remote version test predicate must still accept this value as a remote version in order to display
* as compatible if the mod is present on the client.</p>
@@ -95,27 +94,6 @@ public interface IExtensionPoint<T extends Record>
*/
@SuppressWarnings("JavadocReference") // reference to NetworkConstants, ForgeHooksClient
record DisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) implements IExtensionPoint<DisplayTest> {
public static final String IGNORESERVERONLY = "SERVER_ONLY";
/**
* Ignores any version information coming from the server - use for server only mods
*/
public static final Supplier<DisplayTest> IGNORE_SERVER_VERSION = () -> new DisplayTest(IGNORESERVERONLY, (remoteVersion, isFromServer) -> true);
/**
* Ignores all information and provides no information
* <p>Note: If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or
* Open to LAN), consider setting {@code clientSideOnly=true} in the root of your mods.toml.</p>
*/
public static final Supplier<DisplayTest> IGNORE_ALL_VERSION = () -> new DisplayTest("", (remoteVersion, isFromServer) -> true);
/**
* An optional alternative to {@link #DisplayTest(Supplier, BiPredicate)} which accepts a constant version string
* instead of a {@link Supplier}.
* <p>Internally, the provided version string is wrapped in a Supplier for you.</p>
*/
public DisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
this(() -> version, remoteVersionTest);
}
public static final String IGNORESERVERONLY = "OHNOES\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31";
}
}
@@ -5,15 +5,14 @@
package net.minecraftforge.fml;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
/**
* A mod loading state. During mod loading, the mod loader transitions between states in a defined sorted list of states,
@@ -21,7 +20,6 @@ import java.util.function.Supplier;
*
* @see IModStateProvider
*/
@NullMarked
public interface IModLoadingState {
/**
* {@return the name of this state}
@@ -44,56 +42,45 @@ public interface IModLoadingState {
/**
* {@return a function returning a human-friendly message for this state}
*/
Supplier<String> message();
/**
* @return a function that computes the size of this transition based on the size of the modlist.
* Used to compute progress.
*/
default IntSupplier size() {
return ModList::size;
}
Function<ModList, String> message();
/**
* {@return an optional runnable, which runs before starting the transition from this state to the next}
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
* @see #buildTransition(Executor, Executor, Function, Function)
*/
@Nullable Runnable inlineRunnable();
Optional<Consumer<ModList>> inlineRunnable();
/**
* Builds the transition task for this state with a blank pre-sync and post-sync task.
*
* @param syncExecutor a synchronous executor
* @param syncExecutor a synchronous executor
* @param parallelExecutor a parallel executor
* @param progressBar a progress meter for tracking progress
* @param <T> a type of event fired on the mod-specific event bus
* @return a transition task for this state
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
* @see #buildTransition(Executor, Executor, Function, Function)
*/
default @Nullable CompletableFuture<Void> buildTransition(
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar
) {
return buildTransition(syncExecutor, parallelExecutor, progressBar,
_ -> CompletableFuture.completedFuture(null),
_ -> CompletableFuture.completedFuture(null)
);
default <T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor) {
return buildTransition(syncExecutor, parallelExecutor,
e -> CompletableFuture.runAsync(() -> {}, e),
e -> CompletableFuture.runAsync(() -> {}, e));
}
/**
* Builds the transition task for this state. The pre-sync and post-sync task functions allow the transition builder
* to run these tasks on the same executor as the actual event dispatch and pre/post hooks.
*
* @param syncExecutor a synchronous executor
* @param syncExecutor a synchronous executor
* @param parallelExecutor a parallel executor
* @param progressBar a progress meter for tracking progress
* @param preSyncTask a function which returns a task to run before event pre-dispatch hook
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
* @param preSyncTask a function which returns a task to run before event pre-dispatch hook
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
* @param <T> a type of event fired on the mod-specific event bus
* @return a transition task for this state
*/
@Nullable CompletableFuture<Void> buildTransition(final Executor syncExecutor,
<T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask);
}
@@ -5,47 +5,108 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("unchecked")
public interface IModStateTransition {
static IModStateTransition buildNoopTransition() {
return ModStateTransitionHelper.NOOP;
return new NoopTransition();
}
default CompletableFuture<Void> build(
final String name,
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask
) {
return ModStateTransitionHelper.build(this, name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
default <T extends Event & IModBusEvent>
CompletableFuture<Void> build(final Executor syncExecutor,
final Executor parallelExecutor,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
List<CompletableFuture<Void>> futures = new ArrayList<>();
this.eventFunctionStream().get()
.map(f -> (EventGenerator<T>) f)
.reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, head, ModLoadingStage::currentState, tail))
.ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, last, nextModLoadingStage(), null));
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
final CompletableFuture<Void> eventDispatchCF = ModList.gather(futures).thenCompose(ModList::completableFutureFromExceptionList);
final CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
.thenComposeAsync(n -> eventDispatchCF, parallelExecutor)
.thenApply(r -> {
postSyncTask.apply(syncExecutor);
return null;
});
return this.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
}
default BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextModLoadingStage() {
return ModLoadingStage::nextState;
}
@Nullable
default <T extends IModBusEvent> EventGenerator<T> eventFunction() {
return null;
private <T extends Event & IModBusEvent>
EventGenerator<T> addCompletableFutureTaskForModDispatch(final Executor syncExecutor,
final Executor parallelExecutor,
final List<CompletableFuture<Void>> completableFutures,
final EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState,
final EventGenerator<T> nextGenerator) {
final Executor selectedExecutor = threadSelector().apply(syncExecutor, parallelExecutor);
var preDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) preDispatchHook();
completableFutures.add(preDispatchHook.apply(selectedExecutor, eventGenerator));
completableFutures.add(ModList.get().futureVisitor(eventGenerator, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor)));
var postDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) postDispatchHook();
completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator));
return nextGenerator;
}
Supplier<Stream<EventGenerator<?>>> eventFunctionStream();
ThreadSelector threadSelector();
BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator();
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook();
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook();
interface EventGenerator<T extends IModBusEvent> extends Function<ModContainer, T> {
static <FN extends IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
interface EventGenerator<T extends Event & IModBusEvent> extends Function<ModContainer, T> {
static <FN extends Event & IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
return fn::apply;
}
}
}
record NoopTransition() implements IModStateTransition {
@Override
public Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
return Stream::of;
}
@Override
public ThreadSelector threadSelector() {
return ThreadSelector.SYNC;
}
@Override
public BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator() {
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
}
@Override
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook() {
return (t, f)-> CompletableFuture.completedFuture(null);
}
@Override
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook() {
return (t, f)-> CompletableFuture.completedFuture(null);
}
}
@@ -5,20 +5,63 @@
package net.minecraftforge.fml;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public final class InterModComms {
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {}
public class InterModComms
{
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {
private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
/**
* Deprecated: use {@link #senderModId()}
* @return The modid of the sender. This is supplied by the caller, or by the active mod container context.
* Consider it unreliable.
*/
@Deprecated
public final String getSenderModId() {
return this.senderModId;
}
/**
* Deprecated: use {@link #modId()}
* @return The modid being sent to.
*/
@Deprecated
public final String getModId() {
return this.modId;
}
/**
* Deprecated: use {@link #method()}
* @return The method being sent to.
*/
@Deprecated
public final String getMethod() {
return this.method;
}
/**
* @param <T> The type of the message.
* @return A {@link Supplier} of the message.
* Use {@link #messageSupplier()}
*/
@SuppressWarnings("unchecked")
@Deprecated
public final <T> Supplier<T> getMessageSupplier() {
return (Supplier<T>) this.messageSupplier;
}
}
private static ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
/**
* Send IMC to remote. Sender will default to the active modcontainer, or minecraft if not.
@@ -29,9 +72,8 @@ public final class InterModComms {
* @return true if the message was enqueued for sending (the target modid is loaded)
*/
public static boolean sendTo(final String modId, final String method, final Supplier<?> thing) {
if (!ModList.isLoaded(modId)) return false;
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
.add(new IMCMessage(ModLoadingContext.get().getContainer().getModId(), modId, method, thing));
if (!ModList.get().isLoaded(modId)) return false;
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(ModLoadingContext.get().getActiveContainer().getModId(), modId, method, thing));
return true;
}
@@ -45,9 +87,8 @@ public final class InterModComms {
* @return true if the message was enqueued for sending (the target modid is loaded)
*/
public static boolean sendTo(final String senderModId, final String modId, final String method, final Supplier<?> thing) {
if (!ModList.isLoaded(modId)) return false;
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
.add(new IMCMessage(senderModId, modId, method, thing));
if (!ModList.get().isLoaded(modId)) return false;
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(senderModId, modId, method, thing));
return true;
}
@@ -71,17 +112,19 @@ public final class InterModComms {
* @return All messages
*/
public static Stream<IMCMessage> getMessages(final String modId) {
return getMessages(modId, _ -> true);
return getMessages(modId, s->Boolean.TRUE);
}
private record QueueFilteringSpliterator(
ConcurrentLinkedQueue<IMCMessage> queue,
Predicate<String> methodFilter,
Iterator<IMCMessage> iterator
) implements Spliterator<IMCMessage> {
private static class QueueFilteringSpliterator implements Spliterator<IMCMessage>
{
private final ConcurrentLinkedQueue<IMCMessage> queue;
private final Predicate<String> methodFilter;
private final Iterator<IMCMessage> iterator;
public QueueFilteringSpliterator(final ConcurrentLinkedQueue<IMCMessage> queue, final Predicate<String> methodFilter) {
this(queue, methodFilter, queue.iterator());
this.queue = queue;
this.iterator = queue.iterator();
this.methodFilter = methodFilter;
}
@Override
@@ -95,10 +138,13 @@ public final class InterModComms {
}
@Override
public boolean tryAdvance(final Consumer<? super IMCMessage> action) {
public boolean tryAdvance(final Consumer<? super IMCMessage> action)
{
IMCMessage next;
do {
if (!iterator.hasNext()) {
do
{
if (!iterator.hasNext())
{
return false;
}
next = this.iterator.next();
@@ -113,5 +159,6 @@ public final class InterModComms {
public Spliterator<IMCMessage> trySplit() {
return null;
}
}
}
@@ -16,7 +16,8 @@ import net.minecraftforge.api.distmarker.Dist;
*
* @see Dist
*/
public enum LogicalSide {
public enum LogicalSide
{
/**
* The logical client of the Minecraft game, which interfaces with the player's inputs and renders the player's
* viewpoint.
@@ -26,7 +27,6 @@ public enum LogicalSide {
* @see Dist#CLIENT
*/
CLIENT,
/**
* The logical server of the Minecraft game, responsible for connecting to clients and running the simulation logic
* on the level.
@@ -41,14 +41,16 @@ public enum LogicalSide {
/**
* {@return if this logical side is the server}
*/
public boolean isServer() {
public boolean isServer()
{
return !isClient();
}
/**
* {@return if the logical side is the client}
*/
public boolean isClient() {
public boolean isClient()
{
return this == CLIENT;
}
}
@@ -5,20 +5,22 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.config.IConfigEvent;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.forgespi.language.IModInfo;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -29,35 +31,41 @@ import java.util.function.Supplier;
* a mechanism by which we can wrap actual mod code so that the loader and other
* facilities can treat mods at arms length.
* </p>
*
* @author cpw
*
*/
public abstract class ModContainer {
public abstract class ModContainer
{
protected final String modId;
protected final String namespace;
protected final IModInfo modInfo;
protected ModLoadingStage modLoadingStage;
protected Supplier<?> contextExtension;
protected final Map<ModLoadingStage, Runnable> activityMap = new EnumMap<>(ModLoadingStage.class);
protected final Map<ModLoadingStage, Runnable> activityMap = new HashMap<>();
protected final Map<Class<? extends IExtensionPoint<?>>, Supplier<?>> extensionPoints = new IdentityHashMap<>();
protected final EnumMap<ModConfig.Type, ModConfig> configs = new EnumMap<>(ModConfig.Type.class);
final Set<ModContainer> dependencies = new HashSet<>();
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
public ModContainer(IModInfo info) {
public ModContainer(IModInfo info)
{
this.modId = info.getModId();
// TODO: Currently not reading namespace from configuration..
this.namespace = this.modId;
this.modInfo = info;
this.modLoadingStage = ModLoadingStage.CONSTRUCT;
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest")
.orElseGet(() -> info.getOwningFile().isClientSideOnly() ? "IGNORE_ALL_VERSION" : "MATCH_VERSION");
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest").orElse("MATCH_VERSION"); // missing defaults to DEFAULT type
Supplier<IExtensionPoint.DisplayTest> displayTestSupplier = switch (displayTestString) {
case "MATCH_VERSION" -> // default displaytest checks for version string match
() -> new IExtensionPoint.DisplayTest(() -> this.modInfo.getVersion().toString(),
(incoming, isNetwork) -> Objects.equals(incoming, this.modInfo.getVersion().toString()));
case "IGNORE_SERVER_VERSION" -> // Ignores any version information coming from the server - use for server only mods
IExtensionPoint.DisplayTest.IGNORE_SERVER_VERSION;
() -> new IExtensionPoint.DisplayTest(() -> IExtensionPoint.DisplayTest.IGNORESERVERONLY, (incoming, isNetwork) -> true);
case "IGNORE_ALL_VERSION" -> // Ignores all information and provides no information
IExtensionPoint.DisplayTest.IGNORE_ALL_VERSION;
() -> new IExtensionPoint.DisplayTest(() -> "", (incoming, isNetwork) -> true);
case "NONE" -> null; // NO display test at all - use this if you're going to do your own display test
default -> // any other value throws an exception
throw new IllegalArgumentException("Invalid displayTest value supplied in mods.toml");
@@ -71,35 +79,56 @@ public abstract class ModContainer {
/**
* Errored container state, used for filtering. Does nothing.
*/
ModContainer() {
ModContainer()
{
this.modLoadingStage = ModLoadingStage.ERROR;
modId = "BROKEN";
namespace = "BROKEN";
modInfo = null;
}
/**
* @return the modid for this mod
*/
public final String getModId() {
public final String getModId()
{
return modId;
}
/**
* @return the resource prefix for the mod
*/
public final String getNamespace() {
public final String getNamespace()
{
return namespace;
}
/**
* @return The current loading stage for this mod
*/
public ModLoadingStage getCurrentState() {
public ModLoadingStage getCurrentState()
{
return modLoadingStage;
}
public IModInfo getModInfo() {
public static <T extends Event & IModBusEvent> CompletableFuture<Void> buildTransitionHandler(
final ModContainer target,
final IModStateTransition.EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChangeHandler,
final Executor executor) {
return CompletableFuture
.runAsync(() -> {
ModLoadingContext.get().setActiveContainer(target);
target.activityMap.getOrDefault(target.modLoadingStage, ()->{}).run();
target.acceptEvent(eventGenerator.apply(target));
}, executor)
.whenComplete((mc, exception) -> {
target.modLoadingStage = stateChangeHandler.apply(target.modLoadingStage, exception);
ModLoadingContext.get().setActiveContainer(null);
});
}
public IModInfo getModInfo()
{
return modInfo;
}
@@ -108,31 +137,18 @@ public abstract class ModContainer {
return Optional.ofNullable((T)extensionPoints.getOrDefault(point,()-> null).get());
}
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension)
{
extensionPoints.put(point, extension);
}
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> displayTest);
}
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
registerExtensionPoint(IExtensionPoint.DisplayTest.class, displayTest);
}
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
}
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
}
public void addConfig(final ModConfig modConfig) {
configs.put(modConfig.getType(), modConfig);
}
public void dispatchConfigEvent(IConfigEvent event) {}
public void dispatchConfigEvent(IConfigEvent event) {
configHandler.ifPresent(configHandler->configHandler.accept(event));
}
/**
* Does this mod match the supplied mod?
@@ -147,17 +163,9 @@ public abstract class ModContainer {
*/
public abstract Object getMod();
/**
* @return the mod bus group for this specific mod, or null if there is none.
* Leave null if you use {@link BusGroup#DEFAULT} for your mod lifecycle events.
*/
public BusGroup getModBusGroup() {
return null;
}
/**
* Accept an arbitrary event for processing by the mod. Probably posted to an event bus in the lower level container.
* @param e Event to accept
*/
protected <T extends IModBusEvent> void acceptEvent(T e) {}
protected <T extends Event & IModBusEvent> void acceptEvent(T e) {}
}
@@ -5,22 +5,36 @@
package net.minecraftforge.fml;
import net.minecraftforge.fml.loading.LoadingModList;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.ModFileScanData;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.forgespi.locating.IModFile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -30,131 +44,194 @@ import java.util.stream.Stream;
* Master list of all mods - game-side version. This is classloaded in the game scope and
* can dispatch game level events as a result.
*/
public final class ModList {
private ModList() {}
public class ModList
{
private static Logger LOGGER = LogManager.getLogger();
private static ModList INSTANCE;
private final List<IModFileInfo> modFiles;
private final List<IModInfo> sortedList;
private final Map<String, ModFileInfo> fileById;
private List<ModContainer> mods;
private Map<String, ModContainer> indexedMods;
private List<ModFileScanData> modFileScanData;
private List<ModContainer> sortedContainers;
private static final List<IModFileInfo> MOD_FILES;
private static final List<IModInfo> SORTED_LIST;
private static final Map<String, IModFileInfo> FILE_BY_ID;
private static List<ModContainer> mods;
private static Map<String, ModContainer> indexedMods;
private static List<ModContainer> sortedContainers;
static {
var loadingModFiles = LoadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList();
var loadingSortedList = LoadingModList.getMods();
var modFileInfos = MOD_FILES = loadingModFiles.stream().map(ModFile::getModFileInfo).toList();
SORTED_LIST = loadingSortedList.stream().map(IModInfo.class::cast).toList();
var byId = new HashMap<String, IModFileInfo>();
for (var file : modFileInfos) {
for (var mod : file.getMods())
byId.put(mod.getModId(), mod.getOwningFile());
}
FILE_BY_ID = Map.copyOf(byId);
CrashReportCallables.registerCrashCallable("Mod List", ModList::crashReport);
private ModList(final List<ModFile> modFiles, final List<ModInfo> sortedList)
{
this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).collect(Collectors.toList());
this.sortedList = sortedList.stream().
map(ModInfo.class::cast).
collect(Collectors.toList());
this.fileById = this.modFiles.stream().map(IModFileInfo::getMods).flatMap(Collection::stream).
map(ModInfo.class::cast).
collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile));
CrashReportCallables.registerCrashCallable("Mod List", this::crashReport);
}
private static String getModContainerState(String modId) {
private String getModContainerState(String modId) {
return getModContainerById(modId).map(ModContainer::getCurrentState).map(Object::toString).orElse("NONE");
}
private static String fileToLine(IModFile mf) {
var mainMod = mf.getModInfos().getFirst();
private String fileToLine(IModFile mf) {
return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|%-10.10s|Manifest: %s", mf.getFileName(),
mainMod.getDisplayName(),
mainMod.getModId(),
mainMod.getVersion(),
getModContainerState(mainMod.getModId()),
mf.getModInfos().get(0).getDisplayName(),
mf.getModInfos().get(0).getModId(),
mf.getModInfos().get(0).getVersion(),
getModContainerState(mf.getModInfos().get(0).getModId()),
((ModFileInfo)mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE"));
}
private static String crashReport() {
return "\n" + applyForEachModFile(ModList::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
private String crashReport() {
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
public static void init() {}
public static List<IModFileInfo> getModFiles() {
return MOD_FILES;
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList)
{
INSTANCE = new ModList(modFiles, sortedList);
return INSTANCE;
}
public static IModFileInfo getModFileById(String modid) {
return FILE_BY_ID.get(modid);
public static ModList get() {
return INSTANCE;
}
static void setLoadedMods(final List<ModContainer> modContainers) {
mods = modContainers;
sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c -> SORTED_LIST.indexOf(c.getModInfo()))).toList();
indexedMods = modContainers.stream().collect(Collectors.toUnmodifiableMap(ModContainer::getModId, Function.identity()));
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
thread.setName("modloading-worker-" + thread.getPoolIndex());
// The default sets it to the SystemClassloader, so copy the current one.
thread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
return thread;
}
static void clearLoadedMods() {
mods = List.of();
indexedMods = Map.of();
sortedContainers = List.of();
public List<IModFileInfo> getModFiles()
{
return modFiles;
}
public IModFileInfo getModFileById(String modid)
{
return this.fileById.get(modid);
}
<T extends Event & IModBusEvent> Function<Executor, CompletableFuture<Void>> futureVisitor(
final IModStateTransition.EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChange) {
return executor -> gather(
this.mods.stream()
.map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, stateChange, executor))
.collect(Collectors.toList()))
.thenComposeAsync(ModList::completableFutureFromExceptionList, executor);
}
static CompletionStage<Void> completableFutureFromExceptionList(List<? extends Map.Entry<?, Throwable>> t) {
if (t.stream().noneMatch(e->e.getValue()!=null)) {
return CompletableFuture.completedFuture(null);
} else {
final List<Throwable> throwables = t.stream().filter(e -> e.getValue() != null).map(Map.Entry::getValue).collect(Collectors.toList());
CompletableFuture<Void> cf = new CompletableFuture<>();
final RuntimeException accumulator = new RuntimeException();
cf.completeExceptionally(accumulator);
throwables.forEach(exception -> {
if (exception instanceof CompletionException) {
exception = exception.getCause();
}
if (exception.getSuppressed().length!=0) {
Arrays.stream(exception.getSuppressed()).forEach(accumulator::addSuppressed);
} else {
accumulator.addSuppressed(exception);
}
});
return cf;
}
}
static <V> CompletableFuture<List<Map.Entry<V, Throwable>>> gather(List<? extends CompletableFuture<? extends V>> futures) {
List<Map.Entry<V, Throwable>> list = new ArrayList<>(futures.size());
CompletableFuture<?>[] results = new CompletableFuture[futures.size()];
futures.forEach(future -> {
int i = list.size();
list.add(null);
results[i] = future.whenComplete((result, exception) -> list.set(i, new AbstractMap.SimpleImmutableEntry<>(result, exception)));
});
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
}
void setLoadedMods(final List<ModContainer> modContainers)
{
this.mods = modContainers;
this.sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c->sortedList.indexOf(c.getModInfo()))).toList();
this.indexedMods = modContainers.stream().collect(Collectors.toMap(ModContainer::getModId, Function.identity()));
}
@SuppressWarnings("unchecked")
public static <T> Optional<T> getModObjectById(String modId) {
public <T> Optional<T> getModObjectById(String modId)
{
return getModContainerById(modId).map(ModContainer::getMod).map(o -> (T) o);
}
public static Optional<? extends ModContainer> getModContainerById(String modId) {
return Optional.ofNullable(indexedMods.get(modId));
public Optional<? extends ModContainer> getModContainerById(String modId)
{
return Optional.ofNullable(this.indexedMods.get(modId));
}
public static Optional<? extends ModContainer> getModContainerByObject(Object obj) {
public Optional<? extends ModContainer> getModContainerByObject(Object obj)
{
return mods.stream().filter(mc -> mc.getMod() == obj).findFirst();
}
public static List<IModInfo> getMods() {
return SORTED_LIST;
public List<IModInfo> getMods()
{
return this.sortedList;
}
public static boolean isLoaded(String modTarget) {
return indexedMods.containsKey(modTarget);
public boolean isLoaded(String modTarget)
{
return this.indexedMods.containsKey(modTarget);
}
public static int size() {
public int size()
{
return mods.size();
}
public static List<ModFileScanData> getAllScanData() {
final class LazyInit {
private LazyInit() {}
private static final List<ModFileScanData> MOD_FILE_SCAN_DATA = SORTED_LIST.stream()
.map(IModInfo::getOwningFile)
.filter(Objects::nonNull)
.map(IModFileInfo::getFile)
.distinct()
.map(IModFile::getScanResult)
.toList();
public List<ModFileScanData> getAllScanData()
{
if (modFileScanData == null)
{
modFileScanData = this.sortedList.stream().
map(IModInfo::getOwningFile).
filter(Objects::nonNull).
map(IModFileInfo::getFile).
distinct().
map(IModFile::getScanResult).
collect(Collectors.toList());
}
return LazyInit.MOD_FILE_SCAN_DATA;
return modFileScanData;
}
public static void forEachModFile(Consumer<IModFile> fileConsumer) {
MOD_FILES.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
public void forEachModFile(Consumer<IModFile> fileConsumer)
{
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
}
public static <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
return MOD_FILES.stream().map(IModFileInfo::getFile).map(function);
public <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
return modFiles.stream().map(IModFileInfo::getFile).map(function);
}
public static void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
public void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
indexedMods.forEach(modContainerConsumer);
}
public static void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
sortedContainers.forEach(containerConsumer);
public void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
this.sortedContainers.forEach(containerConsumer);
}
public static List<ModContainer> getLoadedMods() {
return sortedContainers;
}
public static <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
public <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
return indexedMods.values().stream().map(function);
}
private static class UncaughtModLoadingException extends ModLoadingException {
public UncaughtModLoadingException(ModLoadingStage stage, Throwable originalException) {
super(null, stage, "fml.modloading.uncaughterror", originalException);
}
}
}
@@ -5,13 +5,16 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import com.google.common.collect.ImmutableList;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.*;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.LoadingModList;
import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.IModLanguageProvider;
import net.minecraftforge.forgespi.locating.ForgeFeature;
@@ -27,6 +30,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraftforge.fml.Logging.CORE;
import static net.minecraftforge.fml.Logging.LOADING;
@@ -66,239 +70,193 @@ import static net.minecraftforge.fml.Logging.LOADING;
* and completes the mod loading sequence.</dd>
* </dl>
*/
public final class ModLoader {
private ModLoader() {}
public class ModLoader
{
private static final Logger LOGGER = LogManager.getLogger();
private static final ArrayList<ModLoadingException> LOADING_EXCEPTIONS = new ArrayList<>();
private static final ArrayList<ModLoadingWarning> LOADING_WARNINGS = new ArrayList<>();
private static final Consumer<String> STATUS_CONSUMER = StartupNotificationManager.modLoaderConsumer().orElse(_ -> {});
private static final HashSet<IModLoadingState> COMPLETED_STATES = new HashSet<>();
private static boolean loadingStateValid;
private static ModLoader INSTANCE;
private final LoadingModList loadingModList;
private static String computeLanguageList() {
return "\n" + LanguageLoadingProvider
.applyForEach(lp -> lp.name() + '@' + lp.getClass().getPackage().getImplementationVersion())
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
private final List<ModLoadingException> loadingExceptions;
private final List<ModLoadingWarning> loadingWarnings;
private final ModStateManager stateManager;
private boolean loadingStateValid;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<Consumer<String>> statusConsumer = StartupMessageManager.modLoaderConsumer();
private final Set<IModLoadingState> completedStates = new HashSet<>();
private ModList modList;
private ModLoader()
{
INSTANCE = this;
this.loadingModList = FMLLoader.getLoadingModList();
this.loadingExceptions = FMLLoader.getLoadingModList().getErrors().stream()
.flatMap(ModLoadingException::fromEarlyException)
.collect(Collectors.toList());
this.loadingWarnings = FMLLoader.getLoadingModList().getBrokenFiles().stream()
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
.collect(Collectors.toList());
FMLLoader.getLoadingModList().getModFiles().stream()
.filter(ModFileInfo::missingLicense)
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(thisModInfo -> this.loadingExceptions.stream().map(ModLoadingException::getModInfo).anyMatch(otherInfo -> otherInfo == thisModInfo))) //Ignore files where any other mod already encountered an error
.map(modFileInfo -> new ModLoadingException(null, ModLoadingStage.VALIDATE, "fml.modloading.missinglicense", null, modFileInfo.getFile()))
.forEach(this.loadingExceptions::add);
this.stateManager = new ModStateManager();
CrashReportCallables.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo);
CrashReportCallables.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName);
CrashReportCallables.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming);
CrashReportCallables.registerCrashCallable("ModLauncher services", this::computeModLauncherServiceList);
CrashReportCallables.registerCrashCallable("FML Language Providers", this::computeLanguageList);
}
private static String computeModLauncherServiceList() {
private String computeLanguageList() {
return "\n"+FMLLoader.getLanguageLoadingProvider().applyForEach(lp->lp.name() +"@"+ lp.getClass().getPackage().getImplementationVersion()).collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
private String computeModLauncherServiceList() {
final List<Map<String, String>> mods = FMLLoader.modLauncherModList();
return "\n" + mods.stream().map(mod -> mod.getOrDefault("file","nofile") +
' ' + mod.getOrDefault("name", "missing") +
' ' + mod.getOrDefault("type","NOTYPE") +
' ' + mod.getOrDefault("description", "")).
return "\n"+mods.stream().map(mod->mod.getOrDefault("file","nofile")+
" "+mod.getOrDefault("name", "missing")+
" "+mod.getOrDefault("type","NOTYPE")+
" "+mod.getOrDefault("description", "")).
collect(Collectors.joining("\n\t\t","\t\t",""));
}
public static ModLoader get()
{
return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE;
}
/**
* Run on the primary starting thread by ClientModLoader and ServerModLoader
* @param syncExecutor An executor to run tasks on the main thread
* @param parallelExecutor An executor to run tasks on a parallel loading thread pool
* @param periodicTask Optional periodic task to perform on the main thread while other activities run
*/
public static void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
var loadingExceptions = LoadingModList.getErrors().stream()
.flatMap(ModLoadingException::fromEarlyException)
.toList();
ModLoader.LOADING_EXCEPTIONS.addAll(loadingExceptions);
LOADING_WARNINGS.addAll(LoadingModList.getBrokenFiles().stream()
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
.toList());
Set<IModInfo> erroredModInfos;
if (loadingExceptions.isEmpty()) {
erroredModInfos = Collections.emptySet();
} else {
erroredModInfos = Collections.newSetFromMap(new IdentityHashMap<>());
erroredModInfos.addAll(loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
}
LoadingModList.getModFiles().stream()
.filter(ModFileInfo::missingLicense)
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(erroredModInfos::contains)) //Ignore files where any other mod already encountered an error
.map(modFileInfo -> new ModLoadingException(null, ModLoadingStage.VALIDATE, "fml.modloading.missinglicense", null, modFileInfo.getFile()))
.forEach(LOADING_EXCEPTIONS::add);
CrashReportCallables.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo);
CrashReportCallables.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName);
CrashReportCallables.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming);
CrashReportCallables.registerCrashCallable("ModLauncher services", ModLoader::computeModLauncherServiceList);
CrashReportCallables.registerCrashCallable("FML Language Providers", ModLoader::computeLanguageList);
ForgeFeature.registerFeature("javaVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version")));
ForgeFeature.registerFeature("openGLVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.CLIENT, ImmediateWindowHandler.getGLVersion()));
public void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
ForgeFeature.registerFeature("java_version", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.SERVER, System.getProperty("java.version")));
loadingStateValid = true;
statusConsumer.ifPresent(c->c.accept("Waiting for scan to complete"));
FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask);
if (!LOADING_EXCEPTIONS.isEmpty()) {
LOGGER.fatal(CORE, "Error during pre-loading phase", LOADING_EXCEPTIONS.getFirst());
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
ModList.clearLoadedMods();
statusConsumer.ifPresent(c->c.accept("Loading mods"));
final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList(),
loadingModList.getMods());
if (!this.loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0));
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(LOADING_EXCEPTIONS);
}
var failedBounds = new ArrayList<ForgeFeature.Bound>();
for (var mod : LoadingModList.getMods()) {
for (var feature : mod.getForgeFeatures()) {
if (!ForgeFeature.testFeature(FMLEnvironment.dist, feature))
failedBounds.add(feature);
}
throw new LoadingFailedException(loadingExceptions);
}
statusConsumer.ifPresent(c->c.accept("Validating features"));
List<? extends ForgeFeature.Bound> failedBounds = loadingModList.getMods().stream()
.map(ModInfo::getForgeFeatures)
.flatMap(Collection::stream)
.filter(bound -> !ForgeFeature.testFeature(FMLEnvironment.dist, bound))
.toList();
if (!failedBounds.isEmpty()) {
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds);
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
ModList.clearLoadedMods();
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods");
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(failedBounds.stream()
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)))
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb.featureName(), fb.featureBound(), ForgeFeature.featureValue(fb)))
.toList());
}
final var modContainers = new HashMap<String, ModContainer>();
for (var file : LoadingModList.getModFiles()) {
var containers = buildMods(file.getFile());
for (var container : containers)
modContainers.put(container.getModId(), container);
}
if (!LOADING_EXCEPTIONS.isEmpty()) {
LOGGER.fatal(CORE, "Failed to initialize mod containers", LOADING_EXCEPTIONS.getFirst());
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
ModList.clearLoadedMods();
statusConsumer.ifPresent(c->c.accept("Building Mod List"));
final List<ModContainer> modContainers = loadingModList.getModFiles().stream()
.map(ModFileInfo::getFile)
.map(this::buildMods)
.<ModContainer>mapMulti(Iterable::forEach)
.toList();
if (!loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0));
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(LOADING_EXCEPTIONS);
throw new LoadingFailedException(loadingExceptions);
}
// Gather all dependencies, so we can make sure they have finished their transition events before we fire ours
for (var mod : modContainers.values()) {
for (var dep : mod.getModInfo().getDependencies()) {
var target = modContainers.get(dep.getModId());
if (target == null)
continue;
switch (dep.getOrdering()) {
case AFTER:
mod.dependencies.add(target);
break;
case BEFORE:
target.dependencies.add(mod);
break;
default:
break;
}
}
}
ModList.setLoadedMods(modContainers.values().stream().toList());
var stateList = ModStateManager.getStates(ModLoadingPhase.GATHER);
var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
progress.complete();
modList.setLoadedMods(modContainers);
this.modList = modList;
statusConsumer.ifPresent(c->c.accept("Dispatching gathering events"));
stateManager.getStates(ModLoadingPhase.GATHER).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept("Gathering phase complete"));
}
public static void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
var stateList = ModStateManager.getStates(ModLoadingPhase.LOAD);
var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
progress.complete();
public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
statusConsumer.ifPresent(c->c.accept("Performing load phase"));
stateManager.getStates(ModLoadingPhase.LOAD).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept("Mod setup complete"));
}
public static void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
var stateList = ModStateManager.getStates(ModLoadingPhase.COMPLETE);
var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
var modBusGroups = Set.of(
ModList.getLoadedMods().stream()
.map(ModContainer::getModBusGroup)
.filter(Objects::nonNull)
.toArray(BusGroup[]::new)
);
for (var modBusGroup : modBusGroups) {
parallelExecutor.execute(modBusGroup::trim);
}
parallelExecutor.execute(BusGroup.DEFAULT::trim);
STATUS_CONSUMER.accept("Mod loading complete - %d mods loaded".formatted(ModList.size()));
progress.complete();
public void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
statusConsumer.ifPresent(c->c.accept("Performing completion phase"));
stateManager.getStates(ModLoadingPhase.COMPLETE).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())));
}
private static void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) {
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) {
if (!isLoadingStateValid()) {
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
return;
}
progressBar.label(progressBar.name() + " working");
syncExecutor.drive(ticker);
var inlineRunnable = state.inlineRunnable();
if (inlineRunnable != null) handleInlineTransition(inlineRunnable, state, syncExecutor, ticker);
var transition = state.buildTransition(syncExecutor, parallelExecutor, progressBar);
if (transition != null) waitForTransition(state, syncExecutor, ticker, transition);
COMPLETED_STATES.add(state);
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
state.buildTransition(syncExecutor, parallelExecutor).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
completedStates.add(state);
}
private static void handleInlineTransition(final Runnable transition, final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker) {
var pb = StartupMessageManager.addProgressBar("State transition " + state.name() + " running", 0);
syncExecutor.drive(ticker);
transition.run();
syncExecutor.drive(ticker);
pb.complete();
syncExecutor.drive(ticker);
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask) {
if (!isLoadingStateValid()) {
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
return;
}
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
state.buildTransition(syncExecutor, parallelExecutor, preSyncTask, postSyncTask).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
completedStates.add(state);
}
private static void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<Void> transition) {
private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<Void> transition) {
while (!transition.isDone()) {
syncExecutor.drive(ticker);
}
try {
transition.join();
} catch (CompletionException e) {
loadingStateValid = false;
Throwable t = e.getCause();
boolean hasNotModLoadingEx = Arrays.stream(t.getSuppressed())
.anyMatch(obj -> !(obj instanceof ModLoadingException));
if (hasNotModLoadingEx) {
final List<Throwable> notModLoading = Arrays.stream(t.getSuppressed())
.filter(obj -> !(obj instanceof ModLoadingException))
.collect(Collectors.toList());
if (!notModLoading.isEmpty()) {
LOGGER.fatal("Encountered non-modloading exceptions!", e);
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
throw e;
}
final List<ModLoadingException> modLoadingExceptions = Arrays.stream(t.getSuppressed())
.filter(ModLoadingException.class::isInstance)
.map(ModLoadingException.class::cast)
.toList();
.collect(Collectors.toList());
LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size());
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
throw new LoadingFailedException(modLoadingExceptions);
}
}
private static List<ModContainer> buildMods(final IModFile modFile) {
final Map<String, IModInfo> modInfoMap = new HashMap<>();
for (var mod : modFile.getModFileInfo().getMods())
modInfoMap.put(mod.getModId(), mod);
private List<ModContainer> buildMods(final IModFile modFile)
{
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader());
final List<ModContainer> containers = new ArrayList<>();
for (var entry : modFile.getScanResult().getTargets().entrySet()) {
var container = buildModContainerFromTOML(modFile, modInfoMap, entry.getKey(), entry.getValue());
if (container != null)
containers.add(container);
}
final List<ModContainer> containers = modFile.getScanResult().getTargets()
.entrySet()
.stream()
.map(e -> buildModContainerFromTOML(modFile, modInfoMap, e))
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (containers.size() != modInfoMap.size()) {
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().toList();
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().toList();
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList());
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().collect(Collectors.toList());
LOGGER.fatal(LOADING, "File {} constructed {} mods: {}, but had {} mods specified: {}",
LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}",
modFile.getFilePath(),
containers.size(), containerIds,
modInfoMap.size(), modIds);
@@ -311,26 +269,23 @@ public final class ModLoader {
missingMods.removeAll(modIds);
LOGGER.fatal(LOADING, "The following mods are missing, but have classes in the jar: {}", missingMods);
LOADING_EXCEPTIONS.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath()));
loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath()));
}
// remove errored mod containers
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).toList();
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).collect(Collectors.toList());
}
private static ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap,
final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader
) {
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
try {
IModInfo info = modInfoMap.get(modId);
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
if (info == null)
throw new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId);
final String modId = idToProviderEntry.getKey();
final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue();
IModInfo info = Optional.ofNullable(modInfoMap.get(modId)).
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
orElseThrow(()->new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId));
return languageLoader.loadMod(info, modFile.getScanResult(), FMLLoader.getGameLayer());
} catch (ModLoadingException mle) {
// exceptions are caught and added to the error list for later handling
LOADING_EXCEPTIONS.add(mle);
loadingExceptions.add(mle);
// return an errored container instance here, because we tried and failed building a container.
return new ErroredModContainer();
}
@@ -341,67 +296,60 @@ public final class ModLoader {
* and don't want to cause extraneous crashes due to trying to do things that aren't possible in a "broken load"
*/
public static boolean isLoadingStateValid() {
return loadingStateValid;
return get().loadingStateValid;
}
public static boolean hasCompletedState(IModLoadingState state) {
return COMPLETED_STATES.contains(state);
public boolean hasCompletedState(final String stateName) {
IModLoadingState state = stateManager.findState(stateName);
return completedStates.contains(state);
}
public static <T extends IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
public <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event generator to a broken mod state");
return;
}
for (var mod : ModList.getLoadedMods())
mod.acceptEvent(generator.apply(mod));
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(generator.apply(mc)));
}
public static <T extends IModBusEvent> void postEvent(T e) {
public <T extends Event & IModBusEvent> void postEvent(T e) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return;
}
for (var mod : ModList.getLoadedMods())
mod.acceptEvent(e);
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
}
public static <T extends IModBusEvent> T postEventWithReturn(T e) {
public <T extends Event & IModBusEvent> T postEventWithReturn(T e) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return e;
}
for (var mod : ModList.getLoadedMods())
mod.acceptEvent(e);
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
return e;
}
@SuppressWarnings("removal")
public static <T extends IModBusEvent> void postEventWrapContainerInModOrder(T event) {
postEventWithWrapInModOrder(event,
(mc, e) -> ModLoadingContext.get().setActiveContainer(mc),
(mc, e) -> ModLoadingContext.get().setActiveContainer(null)
);
public <T extends Event & IModBusEvent> void postEventWrapContainerInModOrder(T event) {
postEventWithWrapInModOrder(event, (mc, e) -> ModLoadingContext.get().setActiveContainer(mc), (mc, e) -> ModLoadingContext.get().setActiveContainer(null));
}
public static <T extends IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
public <T extends Event & IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return;
}
for (var mod : ModList.getLoadedMods()) {
pre.accept(mod, e);
mod.acceptEvent(e);
post.accept(mod, e);
}
ModList.get().forEachModInOrder(mc -> {
pre.accept(mc, e);
mc.acceptEvent(e);
post.accept(mc, e);
});
}
public static List<ModLoadingWarning> getWarnings() {
return List.copyOf(LOADING_WARNINGS);
public List<ModLoadingWarning> getWarnings()
{
return ImmutableList.copyOf(this.loadingWarnings);
}
public static void addWarning(ModLoadingWarning warning) {
LOADING_WARNINGS.add(warning);
public void addWarning(ModLoadingWarning warning)
{
this.loadingWarnings.add(warning);
}
private static boolean runningDataGen = false;
@@ -410,7 +358,7 @@ public final class ModLoader {
return runningDataGen;
}
private static final class ErroredModContainer extends ModContainer {
private static class ErroredModContainer extends ModContainer {
public ErroredModContainer() {
super();
}
@@ -8,47 +8,36 @@ package net.minecraftforge.fml;
import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.config.IConfigSpec;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import org.slf4j.Logger;
import java.util.function.BiPredicate;
import org.slf4j.LoggerFactory;
import java.util.function.Supplier;
public class ModLoadingContext {
public class ModLoadingContext
{
private static final Logger LOGGER = LogUtils.getLogger();
private static final ThreadLocal<ModLoadingContext> context = ThreadLocal.withInitial(ModLoadingContext::new);
private ModContainer activeContainer;
private Object languageExtension;
private ModLoadingStage stage;
/**
* @deprecated Use the context provided by your language loader in your mod's constructor. Example for Java mods: {@snippet :
* @Mod("examplemod")
* public final class ExampleMod {
* public ExampleMod(FMLJavaModLoadingContext context) {
* // Use context from the param instead of FMLJavaModLoadingContext.get() or ModLoadingContext.get()
* }
* }}
*/
@Deprecated(forRemoval = true, since="1.21.1")
public static ModLoadingContext get() {
return context.get();
}
void setActiveContainer(final ModContainer container) {
private ModContainer activeContainer;
public void setActiveContainer(final ModContainer container) {
this.activeContainer = container;
this.languageExtension = container == null ? null : container.contextExtension.get();
}
/**
* @deprecated Going to be moved to ForgeHooks for Internal use.
*/
@Deprecated(forRemoval = true, since="1.21.1")
public String getActiveNamespace() {
return activeContainer == null ? "minecraft" : activeContainer.getNamespace();
public ModContainer getActiveContainer() {
return activeContainer == null ? ModList.get().getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!")) : activeContainer;
}
public ModContainer getContainer() {
return activeContainer == null
? ModList.getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!"))
: activeContainer;
public String getActiveNamespace() {
return activeContainer == null ? "minecraft" : activeContainer.getNamespace();
}
/**
@@ -58,67 +47,32 @@ public class ModLoadingContext {
* @param <T> The type signature of the extension operator
*/
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
getContainer().registerExtensionPoint(point, extension);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)}.</p>
* @param displayTest The {@link IExtensionPoint.DisplayTest} to register
*/
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
getContainer().registerDisplayTest(() -> displayTest);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest supplier with {@link #registerExtensionPoint(Class, Supplier)}.</p>
* @param displayTest The {@link Supplier<IExtensionPoint.DisplayTest>} to register
*/
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
getContainer().registerDisplayTest(displayTest);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
* creates the DisplayTest instance for you using the provided parameters.</p>
* @see IExtensionPoint.DisplayTest#DisplayTest(String, BiPredicate)
*/
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
* creates the DisplayTest instance for you using the provided parameters.</p>
* @see IExtensionPoint.DisplayTest#DisplayTest(Supplier, BiPredicate)
*/
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
getActiveContainer().registerExtensionPoint(point, extension);
}
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec) {
if (spec.isEmpty()) {
if (spec.isEmpty())
{
// This handles the case where a mod tries to register a config, without any options configured inside it.
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getContainer().getModId());
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getActiveContainer().getModId());
return;
}
getContainer().addConfig(new ModConfig(type, spec, getContainer()));
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer()));
}
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec, String fileName) {
if (spec.isEmpty()) {
if (spec.isEmpty())
{
// This handles the case where a mod tries to register a config, without any options configured inside it.
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getContainer().getModId(), fileName);
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getActiveContainer().getModId(), fileName);
return;
}
getContainer().addConfig(new ModConfig(type, spec, getContainer(), fileName));
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer(), fileName));
}
@SuppressWarnings("unchecked")
public <T> T extension() {
return (T)languageExtension;
@@ -5,16 +5,14 @@
package net.minecraftforge.fml;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
/**
* Implementation of the {@link IModLoadingState} interface.
@@ -26,26 +24,18 @@ import java.util.function.Supplier;
* @param inlineRunnable an optional runnable, which runs before starting the transition from this state to the next
* @param transition optional state transition information
*/
@NullMarked
public record ModLoadingState(
String name,
String previous,
Supplier<String> message,
IntSupplier size,
ModLoadingPhase phase,
@Nullable Runnable inlineRunnable,
@Nullable IModStateTransition transition
) implements IModLoadingState {
public record ModLoadingState(String name, String previous,
Function<ModList, String> message,
ModLoadingPhase phase,
Optional<Consumer<ModList>> inlineRunnable,
Optional<IModStateTransition> transition) implements IModLoadingState {
@Override
public @Nullable CompletableFuture<Void> buildTransition(
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask
) {
if (transition == null) return null;
return transition.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
public <T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
return transition.map(t -> t.build(syncExecutor, parallelExecutor, preSyncTask, postSyncTask));
}
/**
@@ -55,12 +45,9 @@ public record ModLoadingState(
* @param name the name of the state
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) {
return new ModLoadingState(name, previous, () -> "", () -> 0, phase, null, null);
return new ModLoadingState(name, previous, ml -> "", phase, Optional.empty(), Optional.empty());
}
/**
@@ -71,13 +58,11 @@ public record ModLoadingState(
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
*
* @deprecated Use the builder
* @return a mod loading state with state transition information and a default message
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase,
final IModStateTransition transition) {
return of(name, phase).after(previous).withTransition(transition);
return new ModLoadingState(name, previous, ml -> "Processing transition " + name, phase, Optional.empty(), Optional.of(transition));
}
/**
@@ -89,14 +74,11 @@ public record ModLoadingState(
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
* @return a mod loading state with state transition information and a custom message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous,
final Supplier<String> message, final ModLoadingPhase phase,
final Function<ModList, String> message, final ModLoadingPhase phase,
final IModStateTransition transition) {
return of(name, phase).after(previous).message(message).withTransition(transition);
return new ModLoadingState(name, previous, message, phase, Optional.empty(), Optional.of(transition));
}
/**
@@ -108,75 +90,9 @@ public record ModLoadingState(
* @param phase the mod loading phase the state belongs to
* @param inline an optional runnable, which runs before starting the transition from this state to the next
* @return a mod loading state with an inline runnable and default message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase,
final Runnable inline) {
return of(name, phase).after(previous).withInline(inline);
final Consumer<ModList> inline) {
return new ModLoadingState(name, previous, ml -> "Processing work " + name, phase, Optional.of(inline), Optional.empty());
}
public static Builder of(final String name, final ModLoadingPhase phase) {
return new Builder(name, phase);
}
@Override
public boolean equals(Object o) {
return o instanceof ModLoadingState that
&& this.phase == that.phase
&& this.name.equals(that.name)
&& this.previous.equals(that.previous);
}
@Override
public int hashCode() {
int result = phase.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + previous.hashCode();
return result;
}
public static final class Builder {
private final String name;
private final ModLoadingPhase phase;
private String after = "";
private @Nullable Supplier<String> message = null;
private @Nullable IntSupplier size = null;
private Builder(final String name, final ModLoadingPhase phase) {
this.name = name;
this.phase = phase;
}
public Builder after(final ModLoadingState value) { return after(value.name()); }
public Builder after(final String value) {
this.after = value;
return this;
}
public Builder message(final String value) { return message(() -> value); }
public Builder message(final Supplier<String> value) {
this.message = value;
return this;
}
public Builder size(final int value) { return size(() -> value); }
public Builder size(final IntSupplier value) {
this.size = value;
return this;
}
public ModLoadingState empty() {
return new ModLoadingState(name, after, message != null ? message : () -> "", size != null ? size : () -> 0, phase, null, null);
}
public ModLoadingState withTransition(final IModStateTransition transition) {
return new ModLoadingState(name, after, message != null ? message : () -> "Processing transition " + name, size != null ? size : ModList::size, phase, null, transition);
}
public ModLoadingState withInline(final Runnable inline) {
return new ModLoadingState(name, after, message != null ? message : () -> "Processing work " + name, size != null ? size : () -> 0, phase, inline, null);
}
}
}
}
@@ -5,27 +5,42 @@
package net.minecraftforge.fml;
import com.google.common.collect.Streams;
import net.minecraftforge.forgespi.language.IModInfo;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
/**
* @param modInfo Mod info for mod with warning
* @param warningStage The stage where this warning was encountered
* @param i18nMessage I18N message to use for display
* @param context Context for message display
*/
public record ModLoadingWarning(IModInfo modInfo, ModLoadingStage warningStage, String i18nMessage, List<Object> context) {
public ModLoadingWarning(final IModInfo modInfo, final ModLoadingStage warningStage, final String i18nMessage, Object... context) {
this(modInfo, warningStage, i18nMessage, List.of(context));
}
public class ModLoadingWarning
{
/**
* Mod Info for mod with warning
*/
private final IModInfo modInfo;
/**
* The stage where this warning was encountered
*/
private final ModLoadingStage warningStage;
public ModLoadingWarning {
context = List.copyOf(context);
/**
* I18N message to use for display
*/
private final String i18nMessage;
/**
* Context for message display
*/
private final List<Object> context;
public ModLoadingWarning(final IModInfo modInfo, final ModLoadingStage warningStage, final String i18nMessage, Object... context) {
this.modInfo = modInfo;
this.warningStage = warningStage;
this.i18nMessage = i18nMessage;
this.context = Arrays.asList(context);
}
public String formatToString() {
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Stream.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
}
}
@@ -15,19 +15,20 @@ import java.util.function.Function;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
public final class ModStateManager {
private ModStateManager() {}
private static final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
public class ModStateManager {
static ModStateManager INSTANCE;
private final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
static {
public ModStateManager() {
INSTANCE = this;
final var sp = ServiceLoader.load(FMLLoader.getGameLayer(), IModStateProvider.class);
stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, _ -> {})
this.stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, sce->{})
.map(IModStateProvider::getAllStates)
.flatMap(List::stream)
.collect(Collectors.groupingBy(IModLoadingState::phase, () -> new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
.<IModLoadingState>mapMulti(Iterable::forEach)
.collect(Collectors.groupingBy(IModLoadingState::phase, ()->new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
}
public static List<IModLoadingState> getStates(final ModLoadingPhase phase) {
public List<IModLoadingState> getStates(final ModLoadingPhase phase) {
var nodes = stateMap.get(phase);
var lookup = nodes.stream().collect(Collectors.toMap(IModLoadingState::name, Function.identity()));
@@ -35,20 +36,16 @@ public final class ModStateManager {
var dummy = ModLoadingState.empty("", "", phase);
nodes.forEach(graph::addNode);
graph.addNode(dummy);
for (IModLoadingState node : nodes) {
graph.putEdge(lookup.getOrDefault(node.previous(), dummy), node);
}
nodes.forEach(n->graph.putEdge(lookup.getOrDefault(n.previous(), dummy), n));
return TopologicalSort.topologicalSort(graph, Comparator.comparingInt(nodes::indexOf)).stream().filter(st->st!=dummy).toList();
}
public static IModLoadingState findState(final String stateName) {
var result = stateMap.values()
public IModLoadingState findState(final String stateName) {
return stateMap.values()
.stream()
.flatMap(List::stream)
.flatMap(Collection::stream)
.filter(mls -> mls.name().equals(stateName))
.findFirst()
.orElse(null);
if (result != null) return result;
else throw new IllegalArgumentException("Unknown IModLoadingState: " + stateName);
.orElseThrow(() -> new IllegalArgumentException("Unknown IModLoadingState: " + stateName));
}
}
@@ -1,180 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import net.minecraftforge.fml.IModStateTransition.EventGenerator;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
@ApiStatus.Internal
final class ModStateTransitionHelper {
static final IModStateTransition NOOP = new NoopTransition();
record NoopTransition() implements IModStateTransition {
@Override
public ThreadSelector threadSelector() {
return ThreadSelector.SYNC;
}
@Override
public BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator() {
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
}
}
static <V> CompletionStage<Void> completableFutureFromExceptionList(List<FutureResult<V>> t) {
if (t.stream().noneMatch(e -> e.exception() != null)) {
return CompletableFuture.completedFuture(null);
} else {
final var throwables = t.stream().map(FutureResult::exception).filter(Objects::nonNull).toList();
CompletableFuture<Void> cf = new CompletableFuture<>();
final RuntimeException accumulator = new RuntimeException();
cf.completeExceptionally(accumulator);
for (Throwable exception : throwables) {
if (exception instanceof CompletionException) {
exception = exception.getCause();
}
if (exception.getSuppressed().length != 0) {
for (Throwable throwable : exception.getSuppressed()) {
accumulator.addSuppressed(throwable);
}
} else {
accumulator.addSuppressed(exception);
}
}
return cf;
}
}
/// Same as [#completableFutureFromExceptionList(List)] but specialised for a single future
static <V> CompletionStage<Void> completableFutureFromException(FutureResult<V> t) {
if (t.exception() == null) {
return CompletableFuture.completedFuture(null);
} else {
var exception = t.exception();
CompletableFuture<Void> cf = new CompletableFuture<>();
RuntimeException accumulator = new RuntimeException();
cf.completeExceptionally(accumulator);
if (exception instanceof CompletionException) {
exception = exception.getCause();
}
if (exception.getSuppressed().length != 0) {
for (Throwable throwable : exception.getSuppressed()) {
accumulator.addSuppressed(throwable);
}
} else {
accumulator.addSuppressed(exception);
}
return cf;
}
}
record FutureResult<V>(V value, Throwable exception) {}
static <V> CompletableFuture<List<FutureResult<V>>> gather(Collection<? extends CompletableFuture<? extends V>> futures) {
var list = new ArrayList<FutureResult<V>>(futures.size());
var results = new CompletableFuture[futures.size()];
for (var future : futures) {
int i = list.size();
list.add(null);
@SuppressWarnings("unchecked") // Bypass a weird javac error with generic by forcing it to be the exact type we want.
var raw = ((CompletableFuture<V>)future);
results[i] = raw.whenComplete((result, exception) -> list.set(i, new FutureResult<>(result, exception)));
}
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
}
private static <T extends IModBusEvent> CompletableFuture<Void> addCompletableFutureTaskForModDispatch(
final Executor executor,
final ProgressMeter progressBar,
final EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState
) {
var modFutures = new LinkedHashMap<String, CompletableFuture<Void>>();
for (var mod : ModList.getLoadedMods()) {
CompletableFuture<Void> parent = null;
if (mod.dependencies.isEmpty()) {
parent = CompletableFuture.allOf();
} else {
var deps = new CompletableFuture[mod.dependencies.size()];
int idx = 0;
for (var depContainer : mod.dependencies) {
var future = modFutures.get(depContainer.getModId());
if (future == null)
throw new IllegalStateException("Could not find dependency future " + depContainer.getModId() + " for " + mod.getModId());
deps[idx++] = future;
}
parent = CompletableFuture.allOf(deps);
}
@SuppressWarnings("removal")
var dispatch = parent
.thenRunAsync(() -> {
ModLoadingContext.get().setActiveContainer(mod);
var handler = mod.activityMap.get(mod.modLoadingStage);
if (handler != null)
handler.run();
mod.acceptEvent(eventGenerator.apply(mod));
}, executor)
.whenComplete((mc, exception) -> {
mod.modLoadingStage = nextState.apply(mod.modLoadingStage, exception);
progressBar.increment();
ModLoadingContext.get().setActiveContainer(null);
});
modFutures.put(mod.getModId(), dispatch);
}
return gather(modFutures.values()).thenComposeAsync(ModStateTransitionHelper::completableFutureFromExceptionList, executor);
}
static <T extends IModBusEvent> CompletableFuture<Void> build(
final IModStateTransition transition,
final String name,
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask
) {
CompletableFuture<Void> future;
final var executor = transition.threadSelector().apply(syncExecutor, parallelExecutor);
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
.thenRunAsync(() -> progressBar.label(progressBar.name() + ": dispatching " + name), parallelExecutor);
var eventGenerator = transition.eventFunction();
if (eventGenerator != null) {
BiFunction<ModLoadingStage, Throwable, ModLoadingStage> state = transition.nextModLoadingStage();
future = addCompletableFutureTaskForModDispatch(executor, progressBar, eventGenerator, state);
final CompletableFuture<Void> eventDispatchCF = future.handle(FutureResult::new)
.thenCompose(ModStateTransitionHelper::completableFutureFromException);
postEventDispatchCF = postEventDispatchCF.thenComposeAsync(_ -> eventDispatchCF, parallelExecutor);
}
postEventDispatchCF = postEventDispatchCF.thenRun(() -> postSyncTask.apply(syncExecutor));
return transition.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
}
}
@@ -14,20 +14,29 @@ import java.util.concurrent.locks.LockSupport;
import static net.minecraftforge.fml.Logging.LOADING;
public final class ModWorkManager {
public class ModWorkManager {
private static final Logger LOGGER = LogManager.getLogger();
private static final long PARK_TIME = TimeUnit.MILLISECONDS.toNanos(1);
public interface DrivenExecutor extends Executor {
boolean selfDriven();
boolean driveOne();
public sealed interface DrivenExecutor extends Executor {
void drive(Runnable ticker);
}
private record SyncExecutor(ConcurrentLinkedDeque<Runnable> tasks) implements DrivenExecutor {
public SyncExecutor() {
this(new ConcurrentLinkedDeque<>());
default void drive(Runnable ticker) {
if (!selfDriven()) {
while (driveOne()) {
ticker.run();
}
} else {
// park for a bit so other threads can schedule
LockSupport.parkNanos(PARK_TIME);
}
}
}
private static class SyncExecutor implements DrivenExecutor {
private ConcurrentLinkedDeque<Runnable> tasks = new ConcurrentLinkedDeque<>();
private boolean driveOne() {
@Override
public boolean driveOne() {
final Runnable task = tasks.pollFirst();
if (task != null) {
task.run();
@@ -36,35 +45,44 @@ public final class ModWorkManager {
}
@Override
public void execute(final Runnable command) {
tasks.addLast(command);
public boolean selfDriven() {
return false;
}
@Override
public void drive(Runnable ticker) {
ticker.run();
while (true) {
if (!driveOne()) break;
}
public void execute(final Runnable command) {
tasks.addLast(command);
}
}
private record WrappingExecutor(Executor wrapped) implements DrivenExecutor {
private static class WrappingExecutor implements DrivenExecutor {
private final Executor wrapped;
public WrappingExecutor(final Executor executor) {
this.wrapped = executor;
}
@Override
public boolean selfDriven() {
return true;
}
@Override
public boolean driveOne() {
return false;
}
@Override
public void execute(final Runnable command) {
wrapped.execute(command);
}
@Override
public void drive(Runnable ticker) {
// Park for a bit so other threads can schedule
LockSupport.parkNanos(PARK_TIME);
}
}
private static final SyncExecutor syncExecutor = new SyncExecutor();
private static SyncExecutor syncExecutor;
public static DrivenExecutor syncExecutor() {
if (syncExecutor == null)
syncExecutor = new SyncExecutor();
return syncExecutor;
}
@@ -72,8 +90,14 @@ public final class ModWorkManager {
return new WrappingExecutor(executor);
}
private static ForkJoinPool parallelThreadPool;
public static Executor parallelExecutor() {
return LazyInit.PARALLEL_EXECUTOR;
if (parallelThreadPool == null) {
final int loadingThreadCount = FMLConfig.loadingThreadCount();
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
parallelThreadPool = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
}
return parallelThreadPool;
}
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
@@ -84,14 +108,4 @@ public final class ModWorkManager {
return thread;
}
private static final class LazyInit {
private LazyInit() {}
private static final ForkJoinPool PARALLEL_EXECUTOR;
static {
final int loadingThreadCount = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.MAX_THREADS);
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
PARALLEL_EXECUTOR = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
}
}
}
@@ -23,7 +23,7 @@ public final class OptionalMod<T>
return new OptionalMod<>(modId);
}
private static final OptionalMod<?> EMPTY = new OptionalMod<>(true);
private static OptionalMod<?> EMPTY = new OptionalMod<>(true);
private static <T> OptionalMod<T> empty() {
@SuppressWarnings("unchecked")
OptionalMod<T> t = (OptionalMod<T>) EMPTY;
@@ -43,7 +43,7 @@ public final class OptionalMod<T>
private T getValue()
{
if (!searched) {
this.value = ModList.<T>getModObjectById(this.modId).orElse(null);
this.value = ModList.get().<T>getModObjectById(this.modId).orElse(null);
searched = true;
}
return this.value;
@@ -213,8 +213,8 @@ public final class OptionalMod<T>
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj instanceof OptionalMod<?> optionalMod) {
return Objects.equals(optionalMod.modId, modId);
if (obj instanceof OptionalMod) {
return Objects.equals(((OptionalMod)obj).modId, modId);
}
return false;
}
@@ -5,30 +5,19 @@
package net.minecraftforge.fml;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import java.util.Optional;
import java.util.function.Consumer;
public class StartupMessageManager {
public static void addModMessage(final String message) {
StartupNotificationManager.addModMessage(message);
net.minecraftforge.fml.loading.progress.StartupMessageManager.addModMessage(message);
}
public static Optional<Consumer<String>> modLoaderConsumer() {
return StartupNotificationManager.modLoaderConsumer();
return net.minecraftforge.fml.loading.progress.StartupMessageManager.modLoaderConsumer();
}
public static Optional<Consumer<String>> mcLoaderConsumer() {
return StartupNotificationManager.mcLoaderConsumer();
}
public static ProgressMeter addProgressBar(final String barName, final int count) {
return StartupNotificationManager.addProgressBar(barName, count);
}
public static ProgressMeter prependProgressBar(final String barName, final int count) {
return StartupNotificationManager.prependProgressBar(barName, count);
return net.minecraftforge.fml.loading.progress.StartupMessageManager.mcLoaderConsumer();
}
}
@@ -7,6 +7,7 @@ package net.minecraftforge.fml;
import com.google.gson.Gson;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.forgespi.language.IModInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -34,72 +35,81 @@ import java.util.zip.GZIPInputStream;
import static net.minecraftforge.fml.VersionChecker.Status.*;
public class VersionChecker {
public class VersionChecker
{
private static final Logger LOGGER = LogManager.getLogger();
private static final int MAX_HTTP_REDIRECTS = Integer.getInteger("http.maxRedirects", 20);
private static final int HTTP_TIMEOUT_SECS = Integer.getInteger("http.timeoutSecs", 15);
public enum Status {
public enum Status
{
PENDING(),
FAILED(),
UP_TO_DATE(),
OUTDATED(3, true, true),
OUTDATED(3, true),
AHEAD(),
BETA(),
BETA_OUTDATED(6, true, true);
BETA_OUTDATED(6, true);
private final int sheetOffset;
private final boolean draw, animated;
final int sheetOffset;
final boolean draw, animated;
Status() {
Status()
{
this(0, false, false);
}
Status(int sheetOffset, boolean draw, boolean animated) {
Status(int sheetOffset)
{
this(sheetOffset, true, false);
}
Status(int sheetOffset, boolean animated)
{
this(sheetOffset, true, animated);
}
Status(int sheetOffset, boolean draw, boolean animated)
{
this.sheetOffset = sheetOffset;
this.draw = draw;
this.animated = animated;
}
public int getSheetOffset() {
public int getSheetOffset()
{
return sheetOffset;
}
public boolean shouldDraw() {
public boolean shouldDraw()
{
return draw;
}
public boolean isAnimated() {
public boolean isAnimated()
{
return animated;
}
public boolean isOutdated() {
return this == OUTDATED || this == BETA_OUTDATED;
}
}
public record CheckResult(VersionChecker.Status status, ComparableVersion target, Map<ComparableVersion, String> changes, String url) {}
public static void startVersionCheck() {
new Thread("Forge Version Check") {
public static void startVersionCheck()
{
new Thread("Forge Version Check")
{
private HttpClient client;
private String mcVersion;
private String agent;
@Override
public void run() {
if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) {
public void run()
{
if (!FMLConfig.runVersionCheck())
{
LOGGER.info("Global Forge version check system disabled, no further processing.");
return;
}
var mc = ModList.getModContainerById("minecraft");
var forge = ModList.getModContainerById("forge");
this.mcVersion = mc.get().getModInfo().getVersion().toString();
this.agent = "Java-http-client/" + System.getProperty("java.version")
+ " MinecraftForge/" + mcVersion + '-' + forge.get().getModInfo().getVersion();
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS)).build();
gatherMods().forEach(this::process);
}
@@ -109,9 +119,15 @@ public class VersionChecker {
*/
private String openUrlString(URL url, IModInfo mod) throws IOException, URISyntaxException, InterruptedException {
URL currentUrl = url;
var userAgent = this.agent + ' ' + mod.getModId() + '/' + mod.getVersion();
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++) {
final StringBuilder sb = new StringBuilder();
sb.append("Java-http-client/").append(System.getProperty("java.version")).append(' ');
sb.append("MinecraftForge/").append(FMLLoader.versionInfo().mcAndForgeVersion()).append(' ');
sb.append(mod.getModId()).append('/').append(mod.getVersion());
final String userAgent = sb.toString();
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++)
{
var request = HttpRequest.newBuilder()
.uri(currentUrl.toURI())
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS))
@@ -123,7 +139,8 @@ public class VersionChecker {
final HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
int responseCode = response.statusCode();
if (responseCode >= 300 && responseCode <= 399) {
if (responseCode >= 300 && responseCode <= 399)
{
String newLocation = response.headers().firstValue("Location")
.orElseThrow(() -> new IOException("Got a 3xx response code but Location header was null while trying to fetch " + url));
currentUrl = new URL(currentUrl, newLocation);
@@ -133,22 +150,26 @@ public class VersionChecker {
final boolean isGzipEncoded = response.headers().firstValue("Content-Encoding").orElse("").equals("gzip");
final String bodyStr;
try (var inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body();
var bufferedReader = new BufferedReader(new InputStreamReader(inStream))
) {
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
try (InputStream inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body())
{
try (var bufferedReader = new BufferedReader(new InputStreamReader(inStream)))
{
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
}
}
return bodyStr;
}
throw new IOException("Too many redirects while trying to fetch " + url);
}
private void process(IModInfo mod) {
private void process(IModInfo mod)
{
Status status = PENDING;
ComparableVersion target = null;
Map<ComparableVersion, String> changes = null;
String display_url = null;
try {
try
{
if (mod.getUpdateURL().isEmpty()) return;
URL url = mod.getUpdateURL().get();
LOGGER.info("[{}] Starting version check at {}", mod.getModId(), url.toString());
@@ -157,40 +178,54 @@ public class VersionChecker {
LOGGER.debug("[{}] Received version check data:\n{}", mod.getModId(), data);
@SuppressWarnings("unchecked")
Map<String, Object> json = new Gson().fromJson(data, Map.class);
@SuppressWarnings("unchecked")
Map<String, String> promos = (Map<String, String>)json.get("promos");
display_url = (String)json.get("homepage");
var mcVersion = FMLLoader.versionInfo().mcVersion();
String rec = promos.get(mcVersion + "-recommended");
String lat = promos.get(mcVersion + "-latest");
ComparableVersion current = new ComparableVersion(mod.getVersion().toString());
if (rec != null) {
if (rec != null)
{
ComparableVersion recommended = new ComparableVersion(rec);
int diff = recommended.compareTo(current);
if (diff == 0)
status = UP_TO_DATE;
else if (diff < 0) {
else if (diff < 0)
{
status = AHEAD;
if (lat != null) {
if (lat != null)
{
ComparableVersion latest = new ComparableVersion(lat);
if (current.compareTo(latest) < 0) {
if (current.compareTo(latest) < 0)
{
status = OUTDATED;
target = latest;
}
}
} else {
}
else
{
status = OUTDATED;
target = recommended;
}
} else if (lat != null) {
}
else if (lat != null)
{
ComparableVersion latest = new ComparableVersion(lat);
status = current.compareTo(latest) < 0 ? BETA_OUTDATED : BETA;
if (current.compareTo(latest) < 0)
status = BETA_OUTDATED;
else
status = BETA;
target = latest;
} else
}
else
status = BETA;
LOGGER.info("[{}] Found status: {} Current: {} Target: {}", mod.getModId(), status, current, target);
@@ -198,42 +233,52 @@ public class VersionChecker {
changes = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
Map<String, String> tmp = (Map<String, String>)json.get(mcVersion);
if (tmp != null) {
if (tmp != null)
{
List<ComparableVersion> ordered = new ArrayList<>();
for (String key : tmp.keySet()) {
for (String key : tmp.keySet())
{
ComparableVersion ver = new ComparableVersion(key);
if (ver.compareTo(current) > 0 && (target == null || ver.compareTo(target) < 1))
{
ordered.add(ver);
}
}
Collections.sort(ordered);
for (ComparableVersion ver : ordered) {
for (ComparableVersion ver : ordered)
{
changes.put(ver, tmp.get(ver.toString()));
}
}
} catch (Exception e) {
}
catch (Exception e)
{
LOGGER.warn("Failed to process update information", e);
status = FAILED;
}
RESULTS.put(mod, new CheckResult(status, target, changes, display_url));
results.put(mod, new CheckResult(status, target, changes, display_url));
}
}.start();
}
// Gather a list of mods that have opted in to this update system by providing a URL.
private static List<IModInfo> gatherMods() {
private static List<IModInfo> gatherMods()
{
List<IModInfo> ret = new LinkedList<>();
for (IModInfo info : ModList.getMods()) {
for (IModInfo info : ModList.get().getMods()) {
if (info.getUpdateURL().isPresent())
ret.add(info);
}
return ret;
}
private static final Map<IModInfo, CheckResult> RESULTS = new ConcurrentHashMap<>();
private static Map<IModInfo, CheckResult> results = new ConcurrentHashMap<>();
private static final CheckResult PENDING_CHECK = new CheckResult(PENDING, null, null, null);
public static CheckResult getResult(IModInfo mod) {
return RESULTS.getOrDefault(mod, PENDING_CHECK);
public static CheckResult getResult(IModInfo mod)
{
return results.getOrDefault(mod, PENDING_CHECK);
}
}
@@ -14,7 +14,6 @@ import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLPaths;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import java.io.IOException;
@@ -26,65 +25,8 @@ import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;
public class ConfigFileTypeHandler {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH));
private static final ConfigFileTypeHandler CLIENT = new ConfigFileTypeHandler(ModConfig.Type.CLIENT);
private static final ConfigFileTypeHandler COMMON = new ConfigFileTypeHandler(ModConfig.Type.COMMON);
private static final ConfigFileTypeHandler SERVER = new ConfigFileTypeHandler(ModConfig.Type.SERVER);
private final @Nullable ModConfig.Type type;
private @Nullable FileWatcher watcher;
// exists for bin compat
public ConfigFileTypeHandler() {
this(null);
}
private ConfigFileTypeHandler(@Nullable ModConfig.Type type) {
this.type = type;
}
/**
* Gets the handler for the given {@link ModConfig.Type}.
*
* @param type The type to get the handler for
* @return The handler
*/
static ConfigFileTypeHandler get(ModConfig.Type type) {
return switch (type) {
case CLIENT -> CLIENT;
case COMMON -> COMMON;
case SERVER -> SERVER;
};
}
/**
* Gets the {@link FileWatcher} for this handler, creating it if it doesn't exist and/or has been stopped.
*
* @return The watcher
*
* @apiNote This is package-private so modders can't just call {@link FileWatcher#stop()} and fuck up everything.
*/
FileWatcher getWatcher() {
if (this.watcher == null) {
LOGGER.debug(CONFIG, "Starting watcher for handler: {}", this);
this.watcher = new FileWatcher();
}
return this.watcher;
}
/**
* Stops the {@link FileWatcher} for this handler, and sets it to null afterward. Use this instead of
* {@link FileWatcher#stop()}.
*/
void stopWatcher() {
if (this.watcher == null) return;
LOGGER.debug(CONFIG, "Stopping watcher for hander: {}", this);
this.watcher.stop();
this.watcher = null;
}
static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.defaultConfigPath());
public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {
return (c) -> {
@@ -95,7 +37,7 @@ public class ConfigFileTypeHandler {
onFileNotFound((newfile, configFormat)-> setupConfigFile(c, newfile, configFormat)).
writingMode(WritingMode.REPLACE).
build();
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath);
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath.toString());
try
{
configData.load();
@@ -104,9 +46,13 @@ public class ConfigFileTypeHandler {
{
throw new ConfigLoadingException(c, ex);
}
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath);
this.getWatcher().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath);
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath.toString());
try {
FileWatcher.defaultInstance().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath.toString());
} catch (IOException e) {
throw new RuntimeException("Couldn't watch config file", e);
}
return configData;
};
}
@@ -114,16 +60,14 @@ public class ConfigFileTypeHandler {
public void unload(Path configBasePath, ModConfig config) {
Path configPath = configBasePath.resolve(config.getFileName());
try {
this.getWatcher().removeWatch(configPath);
FileWatcher.defaultInstance().removeWatch(configBasePath.resolve(config.getFileName()));
} catch (RuntimeException e) {
LOGGER.error("Failed to remove config {} from tracker!", configPath, e);
LOGGER.error("Failed to remove config {} from tracker!", configPath.toString(), e);
}
}
private static boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
if (!Files.isDirectory(file.getParent())) {
Files.createDirectories(file.getParent());
}
private boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
Files.createDirectories(file.getParent());
Path p = defaultConfigPath.resolve(modConfig.getFileName());
if (Files.exists(p)) {
LOGGER.info(CONFIG, "Loading default config file from path {}", p);
@@ -167,16 +111,17 @@ public class ConfigFileTypeHandler {
}
}
@Override
public String toString() {
return "ConfigFileTypeHandler[" + (type != null ? type : "UNKNOWN") + "]";
}
private static class ConfigWatcher implements Runnable {
private final ModConfig modConfig;
private final CommentedFileConfig commentedFileConfig;
private final ClassLoader realClassLoader;
ConfigWatcher(final ModConfig modConfig, final CommentedFileConfig commentedFileConfig, final ClassLoader classLoader) {
this.modConfig = modConfig;
this.commentedFileConfig = commentedFileConfig;
this.realClassLoader = classLoader;
}
private record ConfigWatcher(
ModConfig modConfig,
CommentedFileConfig commentedFileConfig,
ClassLoader realClassLoader
) implements Runnable {
@Override
public void run() {
// Force the regular classloader onto the special thread
@@ -8,8 +8,6 @@ package net.minecraftforge.fml.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLPaths;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
@@ -18,60 +16,46 @@ import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public final class ConfigTracker {
private ConfigTracker() {}
public class ConfigTracker {
private static final Logger LOGGER = LogUtils.getLogger();
static final Marker CONFIG = MarkerFactory.getMarker("CONFIG");
private static final ConcurrentHashMap<String, ModConfig> FILE_MAP = new ConcurrentHashMap<>();
private static final Map<ModConfig.Type, Set<ModConfig>> CONFIG_SETS = Map.of(
ModConfig.Type.CLIENT, Collections.synchronizedSet(new LinkedHashSet<>()),
ModConfig.Type.COMMON, Collections.synchronizedSet(new LinkedHashSet<>()),
ModConfig.Type.SERVER, Collections.synchronizedSet(new LinkedHashSet<>())
);
private static final ConcurrentHashMap<String, Map<ModConfig.Type, ModConfig>> CONFIGS_BY_MOD = new ConcurrentHashMap<>();
public static final ConfigTracker INSTANCE = new ConfigTracker();
private final ConcurrentHashMap<String, ModConfig> fileMap;
private final EnumMap<ModConfig.Type, Set<ModConfig>> configSets;
private final ConcurrentHashMap<String, Map<ModConfig.Type, ModConfig>> configsByMod;
static void trackConfig(final ModConfig config) {
if (FILE_MAP.containsKey(config.getFileName())) {
LOGGER.error(CONFIG,"Detected config file conflict {} between {} and {}", config.getFileName(), FILE_MAP.get(config.getFileName()).getModId(), config.getModId());
private ConfigTracker() {
this.fileMap = new ConcurrentHashMap<>();
this.configSets = new EnumMap<>(ModConfig.Type.class);
this.configsByMod = new ConcurrentHashMap<>();
this.configSets.put(ModConfig.Type.CLIENT, Collections.synchronizedSet(new LinkedHashSet<>()));
this.configSets.put(ModConfig.Type.COMMON, Collections.synchronizedSet(new LinkedHashSet<>()));
// this.configSets.put(ModConfig.Type.PLAYER, new ConcurrentSkipListSet<>());
this.configSets.put(ModConfig.Type.SERVER, Collections.synchronizedSet(new LinkedHashSet<>()));
}
void trackConfig(final ModConfig config) {
if (this.fileMap.containsKey(config.getFileName())) {
LOGGER.error(CONFIG,"Detected config file conflict {} between {} and {}", config.getFileName(), this.fileMap.get(config.getFileName()).getModId(), config.getModId());
throw new RuntimeException("Config conflict detected!");
}
FILE_MAP.put(config.getFileName(), config);
CONFIG_SETS.get(config.getType()).add(config);
CONFIGS_BY_MOD.computeIfAbsent(config.getModId(), _ -> new EnumMap<>(ModConfig.Type.class)).put(config.getType(), config);
this.fileMap.put(config.getFileName(), config);
this.configSets.get(config.getType()).add(config);
this.configsByMod.computeIfAbsent(config.getModId(), (k)->new EnumMap<>(ModConfig.Type.class)).put(config.getType(), config);
LOGGER.debug(CONFIG, "Config file {} for {} tracking", config.getFileName(), config.getModId());
}
public static void loadConfigs(ModConfig.Type type, Path configBasePath) {
public void loadConfigs(ModConfig.Type type, Path configBasePath) {
LOGGER.debug(CONFIG, "Loading configs type {}", type);
for (ModConfig config : CONFIG_SETS.get(type)) {
openConfig(config, configBasePath);
}
this.configSets.get(type).forEach(config -> openConfig(config, configBasePath));
}
// TODO: [FML] This is only called for the server (outside of forceUnload)
// rethink config implementation for eventual FML rewrite
public static void unloadConfigs(ModConfig.Type type, Path configBasePath) {
public void unloadConfigs(ModConfig.Type type, Path configBasePath) {
LOGGER.debug(CONFIG, "Unloading configs type {}", type);
for (ModConfig config : CONFIG_SETS.get(type)) {
closeConfig(config, configBasePath);
}
ConfigFileTypeHandler.get(type).stopWatcher();
this.configSets.get(type).forEach(config -> closeConfig(config, configBasePath));
}
// If there is a better way to do this, please tell me. Because this is FUCKED
public static void forceUnload() {
// This is how ModStateProvider handles loading configs. So we're doing the same but with unloadConfigs instead
if (FMLEnvironment.dist.isClient())
unloadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get());
unloadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get());
// just in case server watcher is still alive somehow...
ConfigFileTypeHandler.get(ModConfig.Type.SERVER).stopWatcher();
}
private static void openConfig(final ModConfig config, final Path configBasePath) {
private void openConfig(final ModConfig config, final Path configBasePath) {
LOGGER.trace(CONFIG, "Loading config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
final CommentedFileConfig configData = config.getHandler().reader(configBasePath).apply(config);
config.setConfigData(configData);
@@ -79,7 +63,7 @@ public final class ConfigTracker {
config.save();
}
private static void closeConfig(final ModConfig config, final Path configBasePath) {
private void closeConfig(final ModConfig config, final Path configBasePath) {
if (config.getConfigData() != null) {
LOGGER.trace(CONFIG, "Closing config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
// stop the filewatcher before we save the file and close it, so reload doesn't fire
@@ -92,8 +76,8 @@ public final class ConfigTracker {
}
}
public static void loadDefaultServerConfigs() {
CONFIG_SETS.get(ModConfig.Type.SERVER).forEach(modConfig -> {
public void loadDefaultServerConfigs() {
configSets.get(ModConfig.Type.SERVER).forEach(modConfig -> {
final CommentedConfig commentedConfig = CommentedConfig.inMemory();
modConfig.getSpec().correct(commentedConfig);
modConfig.setConfigData(commentedConfig);
@@ -101,16 +85,16 @@ public final class ConfigTracker {
});
}
public static String getConfigFileName(String modId, ModConfig.Type type) {
return Optional.ofNullable(CONFIGS_BY_MOD.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
public String getConfigFileName(String modId, ModConfig.Type type) {
return Optional.ofNullable(configsByMod.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
map(ModConfig::getFullPath).map(Object::toString).orElse(null);
}
public static Map<ModConfig.Type, Set<ModConfig>> configSets() {
return CONFIG_SETS;
public Map<ModConfig.Type, Set<ModConfig>> configSets() {
return configSets;
}
public static ConcurrentHashMap<String, ModConfig> fileMap() {
return FILE_MAP;
public ConcurrentHashMap<String, ModConfig> fileMap() {
return fileMap;
}
}
@@ -5,7 +5,7 @@
package net.minecraftforge.fml.config;
import net.minecraftforge.eventbus.api.event.InheritableEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.Bindings;
import java.util.function.Function;
@@ -13,7 +13,6 @@ import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
public interface IConfigEvent {
// TODO: [FML] Get rid of this indirection somehow
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {}
ConfigConfig CONFIGCONFIG = Bindings.getConfigConfiguration().get();
@@ -30,7 +29,7 @@ public interface IConfigEvent {
ModConfig getConfig();
@SuppressWarnings("unchecked")
default <T extends InheritableEvent & IConfigEvent> T self() {
default <T extends Event & IConfigEvent> T self() {
return (T) this;
}
}
@@ -24,14 +24,15 @@ public class ModConfig
private final ModContainer container;
private final ConfigFileTypeHandler configHandler;
private CommentedConfig configData;
private Callable<Void> saveHandler;
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer container, final String fileName) {
this.type = type;
this.spec = spec;
this.fileName = fileName;
this.container = container;
this.configHandler = ConfigFileTypeHandler.get(type);
ConfigTracker.trackConfig(this);
this.configHandler = ConfigFileTypeHandler.TOML;
ConfigTracker.INSTANCE.trackConfig(this);
}
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer activeContainer) {
@@ -40,9 +41,8 @@ public class ModConfig
private static String defaultConfigName(Type type, String modId) {
// config file name would be "forge-client.toml" and "forge-server.toml"
return modId + '-' + type.extension() + ".toml";
return String.format(Locale.ROOT, "%s-%s.toml", modId, type.extension());
}
public Type getType() {
return type;
}
@@ -5,33 +5,8 @@
package net.minecraftforge.fml.event;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.bus.EventBus;
import net.minecraftforge.eventbus.api.event.InheritableEvent;
import org.jspecify.annotations.NonNull;
/**
* Marker interface for mod lifecycle events dispatched on the
* {@linkplain net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup() mod BusGroup} instead
* of the {@linkplain BusGroup#DEFAULT default BusGroup}.
*
* @apiNote Each mod gets its own unique mod BusGroup instance, obtained from the mod loading context. Most events
* should go on the default BusGroup.
* Marker interface for events dispatched on the ModLifecycle event bus instead of the primary event bus
*/
public interface IModBusEvent extends InheritableEvent {
/**
* @param modEventBusGroup Obtained from {@link net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup()} or your language provider's equivalent.
* @param eventClass The event type you want to get the associated bus for.
*/
static <T extends IModBusEvent> @NonNull EventBus<@NonNull T> getBus(@NonNull BusGroup modEventBusGroup, @NonNull Class<T> eventClass) {
// Do not copy! Temporary solution until the FML rewrite is complete. May throw in future
// without prior notice.
//
// Mods creating their own events should directly assign the result of the create method to a
// static final field in their event class, without wrapping or calling multiple times.
//
// Mods using existing events should instead use the existing static final BUS field in the
// event class (for most events) or getBus(BusGroup) method (for mod bus events).
return EventBus.create(modEventBusGroup, eventClass);
}
public interface IModBusEvent {
}
@@ -7,11 +7,8 @@ package net.minecraftforge.fml.util.thread;
import net.minecraftforge.fml.LogicalSide;
/**
* Attempts to determine the effect side of the current thread, based on know threads in networking.
* This is not guaranteed to be correct, and modders should use other methods to determine the logical side when possible.
*/
public class EffectiveSide {
public class EffectiveSide
{
public static LogicalSide get() {
final ThreadGroup group = Thread.currentThread().getThreadGroup();
return group instanceof SidedThreadGroup ? ((SidedThreadGroup) group).getSide() : LogicalSide.CLIENT;
@@ -7,13 +7,11 @@ package net.minecraftforge.fml.util.thread;
import net.minecraftforge.fml.LogicalSide;
public final class SidedThreadGroups {
public final class SidedThreadGroups
{
public static final SidedThreadGroup CLIENT = new SidedThreadGroup(LogicalSide.CLIENT);
public static final SidedThreadGroup SERVER = new SidedThreadGroup(LogicalSide.SERVER);
public static SidedThreadGroup get(boolean client) {
return client ? CLIENT : SERVER;
private SidedThreadGroups() {
}
private SidedThreadGroups() { }
}
-92
View File
@@ -1,92 +0,0 @@
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
apply from: rootProject.file('build_shared.gradle')
import org.gradle.internal.os.OperatingSystem
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
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
dependencies {
compileOnly(libs.jetbrains.annotations)
implementation(project(':fmlloader'))
implementation(project(':fmlcore'))
implementation(libs.bundles.lwjgl)
implementation('org.slf4j:slf4j-api:1.8.0-beta4')
implementation(libs.jopt.simple)
testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2')
testImplementation('org.powermock:powermock-core:2.0.9')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2')
testRuntimeOnly('org.slf4j:slf4j-jdk14:1.8.0-beta4')
testRuntimeOnly("org.lwjgl:lwjgl::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives")
}
tasks.named('test', Test).configure {
useJUnitPlatform()
}
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer': 'boot'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FML Early Display',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML Early Display',
'Implementation-Vendor': 'Forge Development LLC',
'Implementation-Version': '1.0'
] as LinkedHashMap, 'net/minecraftforge/fml/earlydisplay/')
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlearlydisplay'
pom {
name = project.name
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
}
@@ -1,43 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
public enum ColourScheme {
RED(new Colour(239, 50, 61), new Colour(255, 255, 255)),
BLACK(new Colour(0, 0, 0), new Colour(255, 255, 255));
private final Colour background;
private final Colour foreground;
ColourScheme(final Colour background, final Colour foreground) {
this.background = background;
this.foreground = foreground;
}
public Colour background() {
return background;
}
public Colour foreground() {
return foreground;
}
public record Colour(int red, int green, int blue) {
public float redf() {
return ((float)red)/255f;
}
public float greenf() {
return ((float)green)/255f;
}
public float bluef() {
return ((float)blue)/255f;
}
public int packedint(int a) {
return ((a & 0xff) << 24) | ((blue & 0xff) << 16) | ((green & 0xff) << 8) | (red & 0xff);
}
}
}
@@ -1,651 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import joptsimple.OptionParser;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
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.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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;
import java.util.concurrent.ScheduledFuture;
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.*;
/**
* 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.
* In this way, we can be sure that this will not run before any transformer or injection.
*
* The window itself is spun off into a secondary thread, and is handed off to the main game by Forge.
*
* Because it is created so early, this thread will "absorb" the context from OpenGL.
* Therefore, it is of utmost importance that the Context is made Current for the main thread before handoff,
* otherwise OS X will crash out.
*
* Based on the prior ClientVisualization, with some personal touches.
*/
public class DisplayWindow implements ImmediateWindowProvider {
private static final int[][] GL_VERSIONS = new int[][] {{4,6}, {4,5}, {4,4}, {4,3}, {4,2}, {4,1}, {4,0}, {3,3}};
private static final Logger LOGGER = LoggerFactory.getLogger("EARLYDISPLAY");
private final AtomicBoolean animationTimerTrigger = new AtomicBoolean(true);
private ColourScheme colourScheme = ColourScheme.RED;
private ElementShader elementShader;
private RenderElement.DisplayContext context;
private List<RenderElement> elements;
private int framecount;
private EarlyFramebuffer framebuffer;
private ScheduledFuture<?> windowTick;
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 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);
thread.setName("EarlyDisplay");
thread.setDaemon(true);
return thread;
});
private int fbWidth;
private int fbHeight;
private int fbScale;
private int winWidth;
private int winHeight;
private int winX;
private int winY;
private final Semaphore renderLock = new Semaphore(1);
private boolean maximized;
private String glVersion;
private SimpleFont font;
private Runnable repaintTick = ()->{};
@Override
public String name() {
return "fmlearlywindow";
}
@Override
public Runnable initialize(String[] arguments) {
String mcVersion = FMLLoader.versionInfo().mcVersion();
String forgeVersion = FMLLoader.versionInfo().forgeVersion();
final OptionParser parser = new OptionParser();
var widthopt = parser.accepts("width")
.withRequiredArg().ofType(Integer.class)
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH));
var heightopt = parser.accepts("height")
.withRequiredArg().ofType(Integer.class)
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT));
var maximizedopt = parser.accepts("earlywindow.maximized");
parser.allowsUnrecognizedOptions();
var parsed = parser.parse(arguments);
winWidth = parsed.valueOf(widthopt);
winHeight = parsed.valueOf(heightopt);
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH, winWidth);
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT, winHeight);
fbScale = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_FBSCALE);
if (System.getenv("FML_EARLY_WINDOW_DARK") != null) {
this.colourScheme = ColourScheme.BLACK;
} else {
try {
// check the options file for the colour scheme
var optionLines = Files.readAllLines(FMLPaths.GAMEDIR.get().resolve(Path.of("options.txt")));
var keyName = "darkMojangStudiosBackground:";
for (String line : optionLines) {
if (line.startsWith(keyName)) {
this.colourScheme = line.startsWith("true", keyName.length()) ? ColourScheme.BLACK : ColourScheme.RED;
break;
}
}
} catch (IOException e) {
this.colourScheme = ColourScheme.RED; // fallback to red colourScheme
}
}
this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED);
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + FMLLoader.versionInfo().forgeVersion()));
performanceInfo = new PerformanceInfo();
return start(mcVersion, forgeVersion);
}
private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer
private long nextFrameTime = 0;
/**
* The main render loop.
* renderThread executes this.
*
* Performs initialization and then ticks the screen at 20 fps.
* When the thread is killed, context is destroyed.
*/
private void renderThreadFunc() {
if (!renderLock.tryAcquire()) {
return;
}
try {
long nt;
if ((nt = System.nanoTime()) < nextFrameTime) {
return;
}
nextFrameTime = nt + MINFRAMETIME;
glfwMakeContextCurrent(window);
framebuffer.activate();
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
this.context.elementShader().activate();
this.context.elementShader().updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
paintFramebuffer();
this.context.elementShader().clear();
framebuffer.deactivate();
glViewport(0, 0, fbWidth, fbHeight);
framebuffer.draw(this.fbWidth, this.fbHeight);
// Swap buffers; we're done
glfwSwapBuffers(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
renderLock.release();
}
}
/**
* Render initialization methods called by the Render Thread.
* It compiles the fragment and vertex shaders for rendering text with STB, and sets up basic render framework.
*
* Nothing fancy, we just want to draw and render text.
*/
private void initRender(final @Nullable String mcVersion, final String forgeVersion) {
// This thread owns the GL render context now. We should make a note of that.
glfwMakeContextCurrent(window);
// 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));
elementShader = new ElementShader();
try {
elementShader.init();
} catch (Throwable t) {
LOGGER.error("Crash during shader initialization", t);
crashElegantly("An error occurred initializing shaders.");
}
// Set the clear color based on the colour 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
this.context = new RenderElement.DisplayContext(854, 480, fbScale, elementShader, colourScheme, performanceInfo);
framebuffer = new EarlyFramebuffer(this.context);
try {
this.font = new SimpleFont("Monocraft.ttf", fbScale, 200000, 1 + RenderElement.INDEX_TEXTURE_OFFSET);
} catch (Throwable t) {
LOGGER.error("Crash during font initialization", t);
crashElegantly("An error occurred initializing a font for rendering. "+t.getMessage());
}
this.elements = new ArrayList<>(List.of(
RenderElement.anvil(font),
RenderElement.logMessageOverlay(font),
RenderElement.forgeVersionOverlay(font, mcVersion + "-" + forgeVersion),
RenderElement.performanceBar(font),
RenderElement.progressBars(font)
));
var date = Calendar.getInstance();
if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1))
this.elements.addFirst(RenderElement.squir());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glfwMakeContextCurrent(0);
this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS);
this.performanceTick = 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);
}
/**
* Called every frame by the Render Thread to draw to the screen.
*/
void paintFramebuffer() {
// Clear the screen to our color
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (var itr = this.elements.iterator(); itr.hasNext(); ) {
var element = itr.next();
if (!element.render(context, framecount))
itr.remove();
}
if (animationTimerTrigger.compareAndSet(true, false)) // we only increment the framecount on a periodic basis
framecount++;
}
public void render(int alpha) {
var currentVAO = glGetInteger(GL_VERTEX_ARRAY_BINDING);
var currentFB = glGetInteger(GL_READ_FRAMEBUFFER_BINDING);
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
RenderElement.globalAlpha = alpha;
framebuffer.activate();
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), alpha / 255f);
elementShader.activate();
elementShader.updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
paintFramebuffer();
elementShader.clear();
framebuffer.deactivate();
glBindVertexArray(currentVAO);
glBindFramebuffer(GL_FRAMEBUFFER, currentFB);
}
/**
* Start the window and Render Thread; we're ready to go.
*/
public Runnable start(@Nullable String mcVersion, final String forgeVersion) {
initWindow(mcVersion);
this.initializationFuture = renderScheduler.schedule(() -> initRender(mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS);
return this::periodicTick;
}
private static final String ERROR_URL = "https://links.minecraftforge.net/early-display-errors";
@Override
public String getGLVersion() {
return this.glVersion;
}
private static void crashElegantly(String errorDetails) {
StringBuilder msgBuilder = new StringBuilder(2000);
msgBuilder.append("Failed to initialize graphics window with current settings.\n");
msgBuilder.append("\n\n");
msgBuilder.append("Failure details:\n");
msgBuilder.append(errorDetails);
msgBuilder.append("\n\n");
msgBuilder.append("If you click yes, we will try and open " + ERROR_URL + " in your default browser");
LOGGER.error("ERROR DISPLAY\n"+msgBuilder);
// we show the display on a new dedicated thread
Executors.newSingleThreadExecutor().submit(()-> {
var res = TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", msgBuilder.toString(), "yesno", "error", 0);
if (res != 0) {
try {
Desktop.getDesktop().browse(URI.create(ERROR_URL));
} catch (IOException ioe) {
TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", "Sadly, we couldn't open your browser.\nVisit " + ERROR_URL, "ok", "error", 0);
}
}
System.exit(1);
});
}
/**
* Called to initialize the window when preparing for the Render Thread.
*
* The act of calling glfwInit here creates a concurrency issue; GL doesn't know whether we're gonna call any
* GL functions from the secondary thread and the main thread at the same time.
*
* It's then our job to make sure this doesn't happen, only calling GL functions where the Context is Current.
* 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");
}
long glfwInitEnd = 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)));
// 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);
String vanillaWindowTitle = "Minecraft* ";
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();
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");
throw new IllegalStateException("Can't find a primary monitor");
}
GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor);
if (vidmode == null) {
LOGGER.error("Failed to get the current display video mode.");
crashElegantly("Failed to get current display resolution.\nglfwGetVideoMode 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.");
}, 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++;
continue;
}
LOGGER.info("Trying GL version " + glVersionToTry);
if (showHelpLog && versidx == 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.
Possible solutions:
A) Make sure Minecraft is set to prefer high performance graphics in the OS and/or driver control panel
B) Check for driver updates on the graphics brand's website
C) Try reinstalling your graphics drivers
D) If still not working after trying all of the above, ask for further help on the Forge forums or Discord
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")));
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;
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);
}
glfwGetWindowSize(window, x, y);
this.winWidth = x[0];
this.winHeight = y[0];
glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY);
// 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)));
glfwSetFramebufferSizeCallback(window, this::fbResize);
glfwSetWindowPosCallback(window, this::winMove);
glfwSetWindowSizeCallback(window, this::winResize);
// Show the window
glfwShowWindow(window);
glfwGetWindowPos(window, x, y);
this.winX = x[0];
this.winY = y[0];
glfwGetFramebufferSize(window, x, y);
this.fbWidth = x[0];
this.fbHeight = y[0];
glfwPollEvents();
}
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);
}
}
}
/**
* Hand-off the window to the vanilla game.
* Called on the main thread instead of the game's initialization.
*
* @return the Window we own.
*/
public long setupMinecraftWindow(final int width, final int height, final String title, final long monitorSupplier, final Supplier<Object> backend) {
// wait for the window to actually be initialized
try {
this.initializationFuture.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
Thread.dumpStack();
crashElegantly("We seem to be having trouble initializing the window, waited for 30 seconds");
}
// we have to spin wait for the window ticker
ImmediateWindowHandler.updateProgress("Initializing Game Graphics");
while (!this.windowTick.isDone()) {
this.windowTick.cancel(false);
}
var tries = 0;
var renderlockticket = false;
do {
try {
renderlockticket = renderLock.tryAcquire(100, TimeUnit.MILLISECONDS);
if (++tries > 9) {
Thread.dumpStack();
crashElegantly("We seem to be having trouble handing off the window, tried for 1 second");
}
} catch (InterruptedException e) {
Thread.interrupted();
}
} while (!renderlockticket);
// we don't want the lock, just making sure it's back on the main thread
renderLock.release();
glfwMakeContextCurrent(window);
// 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();
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;
}
@Override
public boolean positionWindow(final Optional<Object> monitor, final IntConsumer widthSetter, final IntConsumer heightSetter, final IntConsumer xSetter, final IntConsumer ySetter) {
widthSetter.accept(this.winWidth);
heightSetter.accept(this.winHeight);
xSetter.accept(this.winX);
ySetter.accept(this.winY);
return true;
}
@Override
public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) {
width.accept(this.fbWidth);
height.accept(this.fbHeight);
}
private Method loadingOverlay;
@SuppressWarnings("unchecked")
@Override
public <T> Supplier<T> loadingOverlay(final Supplier<?> mc, final Supplier<?> ri, final Consumer<Optional<Throwable>> ex, final boolean fade) {
try {
return (Supplier<T>)loadingOverlay.invoke(null, mc, ri, ex, this);
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
}
}
@Override
public void updateModuleReads(final ModuleLayer layer) {
final var FORGE_MODULE = "net.minecraftforge.forge";
var forge_module = layer.findModule(FORGE_MODULE).orElse(null);
if (forge_module == null)
throw new IllegalStateException("Could not find " + FORGE_MODULE + " in " + layer);
getClass().getModule().addReads(forge_module);
var clz = Class.forName(forge_module, "net.minecraftforge.client.loading.ForgeLoadingOverlay");
for (var mtd : clz.getDeclaredMethods()) {
if (Modifier.isStatic(mtd.getModifiers()) && "newInstance".equals(mtd.getName())) {
this.loadingOverlay = mtd;
break;
}
}
if (loadingOverlay == null)
throw new IllegalStateException("Could not find static newInstace method in " + clz.getName());
}
public int getFramebufferTextureId() {
return framebuffer.getTexture();
}
public RenderElement.DisplayContext context() {
return this.context;
}
@Override
public void periodicTick() {
glfwPollEvents();
repaintTick.run();
}
public void addMojangTexture(final int textureId) {
this.elements.addFirst(RenderElement.mojang(textureId, framecount));
}
public void close() {
// Close the Render Scheduler thread
renderScheduler.shutdown();
this.framebuffer.close();
this.context.elementShader().close();
SimpleBufferBuilder.destroy();
}
}
@@ -1,67 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import java.nio.IntBuffer;
import static net.minecraftforge.fml.earlydisplay.RenderElement.clamp;
import static org.lwjgl.opengl.GL32C.*;
public class EarlyFramebuffer {
private final int framebuffer;
private final int texture;
private final RenderElement.DisplayContext context;
EarlyFramebuffer(final RenderElement.DisplayContext context) {
this.context = context;
this.framebuffer = glGenFramebuffers();
this.texture = glGenTextures();
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, this.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, context.width() * context.scale(), context.height() * context.scale(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer)null);
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void activate() {
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
}
void deactivate() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void draw(int windowFBWidth, int windowFBHeight) {
var wscale = ((float)windowFBWidth / this.context.width());
var hscale = ((float)windowFBHeight / this.context.height());
var scale = this.context.scale() * Math.min(wscale, hscale) / 2f;
var wleft = (int)(windowFBWidth * 0.5f - scale * this.context.width());
var wtop = (int)(windowFBHeight * 0.5f - scale * this.context.height());
var wright = (int)(windowFBWidth * 0.5f + scale * this.context.width());
var wbottom = (int)(windowFBHeight * 0.5f + scale * this.context.height());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, this.framebuffer);
final var colour = this.context.colourScheme().background();
glClearColor(colour.redf(), colour.greenf(), colour.bluef(), 1f);
glClear(GL_COLOR_BUFFER_BIT);
// src Y are flipped, since our FB is flipped
glBlitFramebuffer(0, this.context.height() * this.context.scale(), this.context.width() * this.context.scale(), 0, clamp(wleft, 0, windowFBWidth), clamp(wtop, 0, windowFBHeight), clamp(wright, 0, windowFBWidth), clamp(wbottom, 0, windowFBHeight), GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
int getTexture() {
return this.texture;
}
public void close() {
glDeleteTextures(this.texture);
glDeleteFramebuffers(this.framebuffer);
}
}
@@ -1,117 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import static org.lwjgl.opengl.GL32C.*;
public class ElementShader {
private int program;
private int textureUniform;
private int screenSizeUniform;
private int renderTypeUniform;
public void init() {
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
// Bind the source of our shaders to the ones created above
glShaderSource(fragmentShader, """
#version 150 core
uniform sampler2D tex;
uniform int rendertype;
in vec2 fTex;
in vec4 fColour;
out vec4 fragColor;
void main() {
if (rendertype == 0)
fragColor = vec4(1,1,1,texture(tex, fTex).r) * fColour;
if (rendertype == 1)
fragColor = texture(tex, fTex) * fColour;
if (rendertype == 2)
fragColor = fColour;
}
""");
glShaderSource(vertexShader, """
#version 150 core
in vec2 position;
in vec2 tex;
in vec4 colour;
uniform vec2 screenSize;
out vec2 fTex;
out vec4 fColour;
void main() {
fTex = tex;
fColour = colour;
gl_Position = vec4((position/screenSize) * 2 - 1, 0.0, 1.0);
}
""");
// Compile the vertex and fragment elementShader so that we can use them
glCompileShader(vertexShader);
if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) {
throw new IllegalStateException("VertexShader linkage failure. \n" + glGetShaderInfoLog(vertexShader));
}
glCompileShader(fragmentShader);
if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) {
throw new IllegalStateException("FragmentShader linkage failure. \n" + glGetShaderInfoLog(fragmentShader));
}
var program = glCreateProgram();
glBindAttribLocation(program, 0, "position");
glBindAttribLocation(program, 1, "tex");
glBindAttribLocation(program, 2, "colour");
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) {
throw new RuntimeException("ShaderProgram linkage failure. \n" + glGetProgramInfoLog(program));
}
this.program = program;
glDetachShader(program, vertexShader);
glDetachShader(program, fragmentShader);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
textureUniform = glGetUniformLocation(program, "tex");
screenSizeUniform = glGetUniformLocation(program, "screenSize");
renderTypeUniform = glGetUniformLocation(program, "rendertype");
activate();
}
public void activate() {
glUseProgram(program);
}
public void updateTextureUniform(int textureNumber) {
glUniform1i(textureUniform, textureNumber);
}
public void updateScreenSizeUniform(int width, int height) {
glUniform2f(screenSizeUniform, width, height);
}
public void updateRenderTypeUniform(RenderType type) {
glUniform1i(renderTypeUniform, type.ordinal());
}
public void clear() {
glUseProgram(0);
}
public void close() {
glDeleteProgram(program);
}
public enum RenderType {
FONT, TEXTURE, BAR;
}
public int program() {
return program;
}
}
@@ -1,56 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import com.sun.management.OperatingSystemMXBean;
import net.minecraftforge.fml.loading.FMLConfig;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
public class PerformanceInfo {
private static final MemoryMXBean MEMORY_BEAN = ManagementFactory.getMemoryMXBean();
private final boolean showCPUUsage;
private final OperatingSystemMXBean osBean;
private float memory;
private String text;
PerformanceInfo() {
showCPUUsage = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SHOW_CPU);
osBean = showCPUUsage ? ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class) : null;
}
void update() {
final MemoryUsage heapusage = MEMORY_BEAN.getHeapMemoryUsage();
memory = (float) heapusage.getUsed() / heapusage.getMax();
if (!showCPUUsage) {
text = "Memory: %d/%dMB (%.1f%%)".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f);
return;
}
var cpuLoad = osBean.getProcessCpuLoad();
String cpuText;
if (cpuLoad == -1) {
cpuText = "*CPU: %.1f%%".formatted(osBean.getCpuLoad() * 100f);
} else {
cpuText = "CPU: %.1f%%".formatted(cpuLoad * 100f);
}
text = "Memory: %d/%dMB (%.1f%%) %s".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f, cpuText);
}
String text() {
return text;
}
float memory() {
return memory;
}
}
@@ -1,22 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
public class QuadHelper {
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1, int colour) {
bb.pos(x0, y0).tex(u0, v0).colour(colour).endVertex();
bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex();
bb.pos(x0, y1).tex(u0, v1).colour(colour).endVertex();
bb.pos(x1, y1).tex(u1, v1).colour(colour).endVertex();
}
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1) {
bb.pos(x0, y0).tex(u0, v0).endVertex();
bb.pos(x1, y0).tex(u1, v0).endVertex();
bb.pos(x0, y1).tex(u0, v1).endVertex();
bb.pos(x1, y1).tex(u1, v1).endVertex();
}
}
@@ -1,341 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import net.minecraftforge.fml.loading.progress.Message;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
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 {
static final int INDEX_TEXTURE_OFFSET = 5;
private final SimpleBufferBuilder bb;
private final Renderer renderer;
static int globalAlpha = 255;
private int retireCount;
@FunctionalInterface
interface Renderer {
void accept(SimpleBufferBuilder bb, DisplayContext context, int frame);
default Renderer then(Renderer r) {
if (r == null) return this;
return (bb, ctx, frame) -> {
r.accept(bb, ctx, frame);
this.accept(bb, ctx, frame);
};
}
}
@FunctionalInterface
interface TextureRenderer {
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);
}
public record DisplayContext(int width, int height, int scale, ElementShader elementShader, ColourScheme colourScheme, PerformanceInfo performance) {
public int scaledWidth() {
return scale() * width();
}
public int scaledHeight() {
return scale() * height();
}
}
/** @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;
}
public boolean render(DisplayContext ctx, int count) {
this.renderer.accept(bb, ctx, count);
return this.retireCount == 0 || this.retireCount < count;
}
public void retire(final int frame) {
this.retireCount = frame;
}
private static void startupLogMessages(SimpleBufferBuilder bb, SimpleFont font, DisplayContext context) {
List<StartupNotificationManager.AgeMessage> messages = StartupNotificationManager.getMessages();
List<SimpleFont.DisplayText> texts = new ArrayList<>();
for (int i = messages.size() - 1; i >= 0; i--) {
final StartupNotificationManager.AgeMessage pair = messages.get(i);
final float fade = Math.clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f);
if (fade <0.01f) continue;
Message msg = pair.message();
int colour = Math.min((int)(fade * 255f), globalAlpha) << 24 | 0xFFFFFF;
texts.add(new SimpleFont.DisplayText(msg.getText()+"\n", colour));
}
font.generateVerticesForTexts(10, context.scaledHeight() - texts.size() * font.lineSpacing() + font.descent() - 10, bb, texts.toArray(SimpleFont.DisplayText[]::new));
}
public static RenderElement mojang(final int textureId, final int frameStart) {
return new RenderElement((bb, ctx, frame) -> {
var size = 256 * ctx.scale();
var x0 = (ctx.scaledWidth() - 2 * size) / 2;
var y0 = 64 * ctx.scale() + 32;
ctx.elementShader().updateTextureUniform(0);
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
var fade = Math.min((frame - frameStart) * 10, 255);
glBindTexture(GL_TEXTURE_2D, textureId);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, (fade << 24) | 0xFFFFFF);
QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, (fade << 24) | 0xFFFFFF);
bb.draw();
glBindTexture(GL_TEXTURE_2D, 0);
});
}
public static RenderElement logMessageOverlay(SimpleFont font) {
return new RenderElement(RenderElement.initializeText(font, RenderElement::startupLogMessages));
}
public static RenderElement forgeVersionOverlay(SimpleFont font, String version) {
return new RenderElement(RenderElement.initializeText(font, (bb, fnt, ctx)->
font.generateVerticesForTexts(ctx.scaledWidth() - font.stringWidth(version) - 10,
ctx.scaledHeight() - font.lineSpacing() + font.descent() - 10, bb,
new SimpleFont.DisplayText(version, ctx.colourScheme.foreground().packedint(RenderElement.globalAlpha)))));
}
public static RenderElement squir() {
return new RenderElement(RenderElement.initializeTexture("squirrel.png", 45000, 3, (bb, context, size, frame) -> {
var inset = 5f;
var x0 = inset;
var x1 = inset + size[0] * context.scale();
var y0 = inset;
var y1 = inset + size[1] * context.scale();
int fade = (int) (Math.cos(frame * Math.PI / 16) * 16) + 16;
// int fade = 0xff;
var colour = (Math.min(fade, globalAlpha) & 0xff) << 24 | 0xffffff;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, 0f, 1f, colour);
}));
}
public static RenderElement anvil(SimpleFont font) {
return new RenderElement(RenderElement.initializeTexture("forge_anvil.png", 20000, 2, (bb, context, size, frame) -> {
var x0 = context.scaledWidth() - size[0] * context.scale();
var x1 = context.scaledWidth();
var y0 = context.scaledHeight() - size[0] * context.scale() - font.descent() - font.lineSpacing();
var y1 = context.scaledHeight() - font.descent() - font.lineSpacing();
int frameidx = frame % 32;
float framepos = (frameidx * (float)size[0]) / size[1];
float framesize = size[0] / (float)size[1];
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos+framesize, globalAlpha << 24 | 0xFFFFFF);
}));
}
public static RenderElement progressBars(SimpleFont font) {
return new RenderElement((bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame));
}
public static RenderElement performanceBar(SimpleFont font) {
return new RenderElement((bb, ctx, frame) -> RenderElement.memoryInfo(font, bb, ctx, frame));
}
public static void startupProgressBars(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
Renderer acc = null;
var barCount = 2;
List<ProgressMeter> currentProgress = StartupNotificationManager.getCurrentProgress();
var size = currentProgress.size();
var alpha = 0xFF;
for (int i = 0; i < barCount && i < size; i++) {
final ProgressMeter pm = currentProgress.get(i);
Renderer barRenderer = barRenderer(i, alpha, font, pm, context);
acc = barRenderer.then(acc);
alpha >>= 1;
}
if (acc != null)
acc.accept(buffer, context, frameNumber);
}
private static final int BAR_HEIGHT = 20;
private static final int BAR_WIDTH = 400;
private static Renderer barRenderer(int cnt, int alpha, SimpleFont font, ProgressMeter pm, DisplayContext context) {
var barSpacing = font.lineSpacing() - font.descent() + BAR_HEIGHT;
var y = 250 * context.scale() + cnt * barSpacing;
var colour = (alpha << 24) | 0xFFFFFF;
Renderer bar;
if (pm.steps() == 0) {
bar = progressBar(ctx->new int[] {(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f->colour, frame -> indeterminateBar(frame, cnt == 0));
} else {
bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pm.progress()});
}
Renderer label = (bb, ctx, frame) -> renderText(font, text((ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, pm.label().getText(), colour), bb, ctx);
return bar.then(label);
}
private static float[] indeterminateBar(int frame, boolean isActive) {
if (RenderElement.globalAlpha != 0xFF || !isActive) {
return new float[] { 0f, 1f };
} else {
int progress = frame % 100;
return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.clamp((progress + 2) / 100f, 0f, 1f) };
}
}
private static void memoryInfo(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
var y = 10 * context.scale();
PerformanceInfo pi = context.performance();
final int colour = hsvToRGB((1.0f - (float)Math.pow(pi.memory(), 1.5f)) / 3f, 1.0f, 0.5f);
var bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pi.memory()});
var width = font.stringWidth(pi.text());
Renderer label = (bb, ctx, frame) -> renderText(font, text(ctx.scaledWidth() / 2 - width / 2, y + 18, pi.text(), context.colourScheme.foreground().packedint(globalAlpha)), bb, ctx);
bar.then(label).accept(buffer, context, frameNumber);
}
@FunctionalInterface
interface ColourFunction {
int colour(int frame);
}
@FunctionalInterface
interface ProgressDisplay {
float[] progress(int frame);
}
@FunctionalInterface
interface BarPosition {
int[] location(DisplayContext context);
}
public static Renderer progressBar(BarPosition position, ColourFunction colourFunction, ProgressDisplay progressDisplay) {
return (bb, context, frame) -> {
var colour = colourFunction.colour(frame);
var alpha = (colour & 0xFF000000) >> 24;
context.elementShader().updateTextureUniform(0);
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.BAR);
var progress = progressDisplay.progress(frame);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
var inset = 2;
var pos = position.location(context);
var x0 = pos[0];
var x1 = pos[0] + pos[2] + 4 * inset;
var y0 = pos[1];
var y1 = y0 + BAR_HEIGHT;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().foreground().packedint(alpha));
x0 += inset;
x1 -= inset;
y0 += inset;
y1 -= inset;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().background().packedint(RenderElement.globalAlpha));
x1 = x0 + inset + (int)(progress[1] * pos[2]);
x0 += inset + progress[0] * pos[2];
y0 += inset;
y1 -= inset;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, colour);
bb.draw();
};
}
private static Renderer initializeText(SimpleFont font, TextGenerator textGenerator) {
return (bb, context, frame) -> renderText(font, textGenerator, bb, context);
}
private static void renderText(final SimpleFont font, final TextGenerator textGenerator, final SimpleBufferBuilder bb, final DisplayContext context) {
context.elementShader().updateTextureUniform(font.textureNumber());
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.FONT);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
textGenerator.accept(bb, font, context);
bb.draw();
}
private static TextGenerator text(int x, int y, String text, int colour) {
return (bb, font, context) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour));
}
private static Renderer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
int[] imgSize = STBHelper.loadTextureFromClasspath(textureFileName, size, GL_TEXTURE0 + textureNumber + INDEX_TEXTURE_OFFSET);
return (bb, ctx, frame) -> {
ctx.elementShader().updateTextureUniform(textureNumber + INDEX_TEXTURE_OFFSET);
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
renderTexture(bb, ctx, frame, imgSize, positionAndColour);
};
}
private static void renderTexture(SimpleBufferBuilder bb, DisplayContext context, int frame, int[] size, TextureRenderer positionAndColour) {
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
positionAndColour.accept(bb, context, size, frame);
bb.draw();
}
@Deprecated(since = "1.21.5", forRemoval = true)
public static float clamp(float num, float min, float max) {
return Math.clamp(num, min, max);
}
public static int clamp(int num, int min, int max) {
return Math.min(max, Math.max(num, min));
}
public static int hsvToRGB(float hue, float saturation, float value) {
int i = (int)(hue * 6.0F) % 6;
float f = hue * 6.0F - (float)i;
float f1 = value * (1.0F - saturation);
float f2 = value * (1.0F - f * saturation);
float f3 = value * (1.0F - (1.0F - f) * saturation);
float f4;
float f5;
float f6;
switch(i) {
case 0:
f4 = value;
f5 = f3;
f6 = f1;
break;
case 1:
f4 = f2;
f5 = value;
f6 = f1;
break;
case 2:
f4 = f1;
f5 = value;
f6 = f3;
break;
case 3:
f4 = f1;
f5 = f2;
f6 = value;
break;
case 4:
f4 = f3;
f5 = f1;
f6 = value;
break;
case 5:
f4 = value;
f5 = f1;
f6 = f2;
break;
default:
throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value);
}
int j = clamp((int)(f4 * 255.0F), 0, 255);
int k = clamp((int)(f5 * 255.0F), 0, 255);
int l = clamp((int)(f6 * 255.0F), 0, 255);
return 0xFF << 24 | j << 16 | k << 8 | l;
}
}
@@ -1,65 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.STBImage;
import org.lwjgl.system.MemoryUtil;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.util.Objects;
import static org.lwjgl.opengl.GL32C.*;
public class STBHelper {
public static ByteBuffer readFromClasspath(final String name, int initialCapacity) {
ByteBuffer buf;
try (var channel = Channels.newChannel(
Objects.requireNonNull(STBHelper.class.getClassLoader().getResourceAsStream(name), "The resource "+name+" cannot be found"))) {
buf = BufferUtils.createByteBuffer(initialCapacity);
while (true) {
var readbytes = channel.read(buf);
if (readbytes == -1) break;
if (buf.remaining() == 0) { // extend the buffer by 50%
var newBuf = BufferUtils.createByteBuffer(buf.capacity() * 3 / 2);
buf.flip();
newBuf.put(buf);
buf = newBuf;
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
buf.flip();
return MemoryUtil.memSlice(buf); // we trim the final buffer to the size of the content
}
public static int[] loadTextureFromClasspath(String file, int size, int textureNumber) {
int[] lw = new int[1];
int[] lh = new int[1];
int[] lc = new int[1];
var img = loadImageFromClasspath(file, size, lw, lh, lc);
var texid = glGenTextures();
glActiveTexture(textureNumber);
glBindTexture(GL_TEXTURE_2D, texid);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lw[0], lh[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, img);
glActiveTexture(GL_TEXTURE0);
MemoryUtil.memFree(img);
return new int[] {lw[0], lh[0]};
}
public static ByteBuffer loadImageFromClasspath(String file, int size, int[] width, int[] height, int[] channels) {
ByteBuffer buf = STBHelper.readFromClasspath(file, size);
return STBImage.stbi_load_from_memory(buf, width, height, channels, 4);
}
}
@@ -1,517 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.system.MemoryUtil;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static org.lwjgl.opengl.GL32C.*;
/**
* A very simple, Mojang inspired BufferBuilder.
* <em>This has been customized for 2d rendering such as text and simple planar textures</em>
* <p>
* Not bound to any specific format, ideally should be held onto for re-use.
* <p>
* Can be used for 'immediate mode' style rendering using {@link #draw()}, or
* upload to external vertex arrays for proper instancing using {@link #finishAndUpload()}.
* <p>
* This is a Triangles only buffer, all data uploaded is in Triangles.
* Quads are converted to triangles using {@code 0, 1, 2, 0, 2, 3}.
* <p>
* Any given {@link Format} should have its individual {@link Element} components
* buffered in the order specified by the {@link Format},
* followed by an {@link #endVertex()} call to prepare for the next vertex.
* <p>
* It is illegal to buffer primitives in any format other than the one specified to
* {@link #begin(Format, Mode)}.
*
* @author covers1624
*/
public class SimpleBufferBuilder implements Closeable {
private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false);
private static final int[] VERTEX_ARRAYS = new int[Format.values().length];
private static final int[] VERTEX_BUFFERS = new int[Format.values().length];
private static final int[] VERTEX_BUFFER_LENGTHS = new int[Format.values().length];
private static int elementBuffer = 0;
private static int elementBufferVertexLength = 0;
static {
Arrays.fill(VERTEX_ARRAYS, 0);
Arrays.fill(VERTEX_BUFFERS, 0);
Arrays.fill(VERTEX_BUFFER_LENGTHS, 0);
}
private long bufferAddr; // Pointer to the backing buffer.
private ByteBuffer buffer; // ByteBuffer view of the backing buffer.
private Format format; // The current format we are buffering.
private Mode mode; // The current mode we are buffering.
private boolean building; // If we are building the buffer.
private int elementIndex; // The current element index we are buffering. if elementIndex == format.types.length, we expect 'endVertex'
private int index; // The current index into the buffer we are writing to.
private int vertices; // The number of complete vertices we have buffered.
/**
* Create a new SimpleBufferBuilder with an initial capacity.
* <p>
* The buffer will be doubled as required.
* <p>
* Generally picking a small number, around 128/256 should be a
* safe bet. Provided you cache your buffers, it should not mean much overall.
*
* @param capacity The initial capacity in bytes.
*/
public SimpleBufferBuilder(int capacity) {
bufferAddr = ALLOCATOR.malloc(capacity);
buffer = MemoryUtil.memByteBuffer(bufferAddr, capacity);
}
public static void destroy() {
glDeleteBuffers(VERTEX_BUFFERS);
glDeleteBuffers(elementBuffer);
glDeleteVertexArrays(VERTEX_ARRAYS);
}
private static void ensureElementBufferLength(int vertices) {
if (elementBufferVertexLength >= vertices) {
return;
}
// treating it as immutable storage, even though it's not
final var newElementBuffer = glGenBuffers();
var newElementBufferVertexLength = Math.max(1024, elementBufferVertexLength);
while (newElementBufferVertexLength < vertices) {
newElementBufferVertexLength *= 2;
}
final var oldIndexCount = elementBufferVertexLength + elementBufferVertexLength / 2;
final var newIndexCount = newElementBufferVertexLength + newElementBufferVertexLength / 2;
// allocate new buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, newElementBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, newIndexCount * 4L, GL_STATIC_DRAW);
// mapping avoids creating additional CPU copies of the data
// unsynchronized is fine because this is a brand-new buffer, and the old contents will be copied in afterward
// also can invalidate the whole buffer too, similarly because brand new, don't care what was there before
final var mappingOffset = oldIndexCount * 4;
final var mappingSize = (newIndexCount - oldIndexCount) * 4;
final var mappedBuffer = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, mappingOffset, mappingSize, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
if(mappedBuffer == null){
throw new NullPointerException("OpenGL buffer mapping failed");
}
final int quads = newElementBufferVertexLength / 4;
final int oldQuads = elementBufferVertexLength / 4;
// generate indices for the extension to the buffer
for (int i = oldQuads; i < quads; i++) {
// Quads are a bit different, we need to emit 2 triangles such that
// when combined they make up a single quad.
mappedBuffer.putInt(i * 4 + 0).putInt(i * 4 + 1).putInt(i * 4 + 2);
mappedBuffer.putInt(i * 4 + 1).putInt(i * 4 + 3).putInt(i * 4 + 2);
}
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
if (elementBuffer != 0) {
// copy old data from previous element buffer
glBindBuffer(GL_COPY_READ_BUFFER, elementBuffer);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, mappingOffset);
glBindBuffer(GL_COPY_READ_BUFFER, 0);
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDeleteBuffers(elementBuffer);
elementBuffer = newElementBuffer;
elementBufferVertexLength = newElementBufferVertexLength;
}
/**
* Start building a new set of vertex data in the
* given format and mode.
*
* @param format The format to start building in.
* @param mode The mode to start building in.
*/
public SimpleBufferBuilder begin(Format format, Mode mode) {
if (bufferAddr == MemoryUtil.NULL) {
throw new IllegalStateException("Buffer has been freed."); // You already free'd the buffer
}
if (building) {
throw new IllegalStateException("Already building."); // Your already building verticies.
}
this.format = format;
this.mode = mode;
building = true;
elementIndex = 0;
ensureSpace(format.stride);
// Rewind ready for new data.
buffer.rewind();
buffer.limit(buffer.capacity());
return this;
}
/**
* Buffer a position element.
*
* @param x The x.
* @param y The y.
* @param z The z.
* @return The same builder.
*/
public SimpleBufferBuilder pos(float x, float y) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.POS) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes that our POS element specifies the FLOAT data type.
buffer.putFloat(index + 0, x);
buffer.putFloat(index + 4, y);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a texture element.
*
* @param u The u.
* @param v The v.
* @return The same builder.
*/
public SimpleBufferBuilder tex(float u, float v) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.TEX) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our TEX element specifies the FLOAT data type.
buffer.putFloat(index + 0, u);
buffer.putFloat(index + 4, v);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a color element.
*
* @param r The red component. (0-1)
* @param g The green component. (0-1)
* @param b The blue component. (0-1)
* @param a The alpha component. (0-1)
* @return The same buffer.
*/
public SimpleBufferBuilder colour(float r, float g, float b, float a) {
// Expand floats to 0-255 and forward.
return colour((byte) (r * 255F), (byte) (g * 255F), (byte) (b * 255F), (byte) (a * 255F));
}
/**
* @see ColourScheme.Colour#packedint(int)
* @param packedColor an ABGR packed int
* @return the same buffer.
*/
public SimpleBufferBuilder colour(int packedColor) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
buffer.putInt(index + 0, packedColor);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a color element.
*
* @param r The red component. (0-255)
* @param g The green component. (0-255)
* @param b The blue component. (0-255)
* @param a The alpha component. (0-255)
* @return The same buffer.
*/
public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
buffer.put(index + 0, r);
buffer.put(index + 1, g);
buffer.put(index + 2, b);
buffer.put(index + 3, a);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* End building the current vertex and prepare for the next.
*
* @return The same builder.
*/
public SimpleBufferBuilder endVertex() {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex != format.types.length) throw new IllegalStateException("Expected " + format.types[elementIndex]); // You did not finish building the vertex.
// Reset elementIndex
elementIndex = 0;
// Increment the number of vertices we have so far buffered.
vertices++;
// Make sure there is space for the next vertex.
ensureSpace(format.stride);
return this;
}
// Checks there is enough space in the buffer for specified number of bytes.
// If there is not enough space, the buffer is increased by 50%.
private void ensureSpace(int newBytes) {
int cap = buffer.capacity();
if (index + newBytes > cap) {
int newCap = Math.max(3 * cap / 2, 3 * newBytes / 2);
bufferAddr = ALLOCATOR.realloc(bufferAddr, newCap);
buffer = MemoryUtil.memByteBuffer(bufferAddr, newCap);
buffer.rewind();
}
}
/**
* Upload the current buffer.
* <p>
* This will bind a {@link org.lwjgl.opengl.GL32C#GL_ARRAY_BUFFER} and {@link org.lwjgl.opengl.GL32C#GL_ELEMENT_ARRAY_BUFFER}
* <p>
* The vertex data and index data is uploaded to their respective buffers.
* <p>
* Uploading the buffers finishes drawing and resets for the next buffer operation.
* <p>
* This should not be called in conjunction with {@link #draw()}
*
* @return The number of indexes that were uploaded.
*/
public int finishAndUpload() {
if (!building) throw new IllegalStateException("Not building.");
int indices;
try {
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex.
if (elementIndex != 0) throw new IllegalStateException("Not finished building vertex, Expected: " + format.types[elementIndex]); // You didn't finish building your vertex data.
if (vertices == 0) return 0; // No vertices buffered, lets not do anything.
if (vertices % mode.vertices != 0) throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS
// Reset position to 0, limit the buffer to our index.
buffer.position(0);
buffer.limit(index);
// Upload the raw vertex data in dynamic mode.
final int vbo = VERTEX_BUFFERS[format.ordinal()];
final int vboSize = VERTEX_BUFFER_LENGTHS[format.ordinal()];
glBindBuffer(GL_ARRAY_BUFFER, vbo);
if (vboSize < index) {
// expand buffer, it's not big enough
var newVBOSize = Math.max(1024, vboSize);
while (newVBOSize < index){
newVBOSize *= 2;
}
// because everything is overwritten anyway, we can do an in-place reallocation
glBufferData(GL_ARRAY_BUFFER, newVBOSize, GL_DYNAMIC_DRAW);
VERTEX_BUFFER_LENGTHS[format.ordinal()] = newVBOSize;
}
glBufferSubData(GL_ARRAY_BUFFER, 0, buffer);
// The number of indices for triangles is equal to our vertex count, as that is
// what we operate in. However, for Quads, we have exactly vertices + vertices / 2
// vertices once we convert the quads to triangles.
indices = mode == Mode.TRIANGLES ? vertices : vertices + vertices / 2;
if (mode == Mode.QUADS) {
ensureElementBufferLength(vertices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
}
return indices;
} finally {
// Reset builder state for next begin call.
building = false;
vertices = 0;
index = 0;
}
}
/**
* Upload and draw this buffer using one of a number of re-usable set of buffers.
* <p>
* This will immediately upload the buffer, resetting this builder for the next
* buffer operation, and draw the uploaded data.
* <p>
* You will need to bind shaders, textures, etc, before calling this function.
*/
public void draw() {
if (!building) throw new IllegalStateException("Not building.");
int vao = VERTEX_ARRAYS[format.ordinal()];
int vbo = VERTEX_BUFFERS[format.ordinal()];
if (vao == 0) {
// These 3 buffers are paired, you can't allocate one without the others.
assert vbo == 0;
// Make new vertex array and buffers!
vao = glGenVertexArrays();
vbo = glGenBuffers();
// Cache the vertex array and buffers for future re-use.
VERTEX_ARRAYS[format.ordinal()] = vao;
VERTEX_BUFFERS[format.ordinal()] = vbo;
// Ask our Format to set up its data layout for the vertex array.
// but only once, the VAO saves this state
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
format.bind();
format.enable();
}
// Bind the vertex array and buffers!
glBindVertexArray(vao);
// Upload the data.
int indices = finishAndUpload();
if (mode == Mode.QUADS) {
glDrawElements(GL_TRIANGLES, indices, GL_UNSIGNED_INT, 0);
} else {
glDrawArrays(GL_TRIANGLES, 0, indices);
}
// Unbind the vertex array.
glBindVertexArray(0);
}
/**
* Clear this builder's cached buffer.
* <p>
* If you are completely done, call {@link #destroy()}
*/
@Override
public void close() {
ALLOCATOR.free(bufferAddr);
bufferAddr = MemoryUtil.NULL;
}
/**
* Represents a primitive mode that this builder is capable of buffering in.
*/
public enum Mode {
TRIANGLES(3),
QUADS(4),
;
public final int vertices;
Mode(int vertices) {
this.vertices = vertices;
}
}
/**
* Specifies a vertex element with a specific data type, number of primitives and a size in bytes.
*/
public enum Element {
POS(GL_FLOAT, 2, 2 * 4),
TEX(GL_FLOAT, 2, 2 * 4),
COLOR(GL_UNSIGNED_BYTE, 4, 4);
public final int glType;
public final int count;
public final int width;
Element(int glType, int count, int width) {
this.glType = glType;
this.count = count;
this.width = width;
}
}
/**
* Specifies a combination of vertex elements.
*/
public enum Format {
POS(Element.POS),
POS_TEX(Element.POS, Element.TEX),
POS_COLOR(Element.POS, Element.COLOR),
POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR);
private final Element[] types;
public final int stride;
Format(Element... types) {
this.types = types;
// Stride is the width of each vertex in bytes.
stride = Arrays.stream(types).mapToInt(e -> e.width).sum();
}
/**
* set up the attribute pointers for this format.
* <p>
* Assumes that an array buffer is already bound and ready to go.
*/
public void bind() {
int offset = 0;
// Set up the pointers that tell GL where our interleaved
// vertex data is in the buffers.
for (int i = 0; i < types.length; i++) {
Element type = types[i];
switch (type.glType) {
case GL_FLOAT -> glVertexAttribPointer(i, type.count, GL_FLOAT, false, stride, offset);
case GL_UNSIGNED_BYTE -> glVertexAttribPointer(i, type.count, GL_UNSIGNED_BYTE, true, stride, offset);
default -> throw new IllegalStateException("Unknown glType, I don't know how to bind this vertex element: " + type);
}
// add to the offset for the next element.
offset += type.width;
}
}
/**
* Enables the vertex attributes this format contains.
*/
public void enable() {
for (int i = 0; i < types.length; i++) {
glEnableVertexAttribArray(i);
}
}
/**
* Disables the vertex attributes this format contains.
*/
public void disable() {
for (int i = 0; i < types.length; i++) {
glDisableVertexAttribArray(i);
}
}
}
}
@@ -1,180 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.STBTTAlignedQuad;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTTPackContext;
import org.lwjgl.stb.STBTTPackRange;
import org.lwjgl.stb.STBTTPackedchar;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static org.lwjgl.opengl.GL32C.*;
import static org.lwjgl.stb.STBTruetype.*;
import static org.lwjgl.system.MemoryUtil.NULL;
public class SimpleFont {
private final int textureNumber;
private final int lineSpacing;
private final int descent;
private static final int GLYPH_COUNT = 127 - 32;
private final Glyph[] glyphs;
private record Glyph(char c, int charwidth, int[] pos, float[] uv) {
Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) {
final var x0 = pos.x() + pos()[0];
final var y0 = pos.y() + pos()[1];
final var x1 = pos.x() + pos()[2];
final var y1 = pos.y() + pos()[3];
bb.pos(x0, y0).tex(uv()[0], uv()[1]).colour(colour).endVertex();
bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex();
bb.pos(x0, y1).tex(uv()[0], uv()[3]).colour(colour).endVertex();
bb.pos(x1, y1).tex(uv()[2], uv()[3]).colour(colour).endVertex();
return new Pos(pos.x()+charwidth(), pos.y(), pos.minx());
}
}
/**
* Build the font and store it in the textureNumber location
*/
public SimpleFont(String fontName, int scale, int bufferSize, int textureNumber) {
ByteBuffer buf = STBHelper.readFromClasspath(fontName, bufferSize);
var info = STBTTFontinfo.create();
if (!stbtt_InitFont(info, buf)) {
throw new IllegalStateException("Bad font");
}
var ascent = new float[1];
var descent = new float[1];
var lineGap = new float[1];
int fontSize = 24;
stbtt_GetScaledFontVMetrics(buf, 0, fontSize, ascent, descent, lineGap);
this.lineSpacing = (int)(ascent[0] - descent[0] + lineGap[0]);
this.descent = (int)Math.floor(descent[0]);
int fontTextureId = glGenTextures();
glActiveTexture(GL_TEXTURE0+textureNumber);
this.textureNumber = textureNumber;
glBindTexture(GL_TEXTURE_2D, fontTextureId);
try (var packedchars = STBTTPackedchar.malloc(GLYPH_COUNT)) {
int texwidth = 256;
int texheight = 128;
try (STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1)) {
var bitmap = BufferUtils.createByteBuffer(texwidth * texheight);
try (STBTTPackRange packRange = STBTTPackRange.malloc()) {
packRanges.put(packRange.set(fontSize, 32, null, GLYPH_COUNT, packedchars, (byte) 1, (byte) 1));
packRanges.flip();
}
try (STBTTPackContext pc = STBTTPackContext.malloc()) {
stbtt_PackBegin(pc, bitmap, texwidth, texheight, 0, 1, NULL);
stbtt_PackSetOversampling(pc, 1, 1);
stbtt_PackSetSkipMissingCodepoints(pc, true);
stbtt_PackFontRanges(pc, buf, 0, packRanges);
stbtt_PackEnd(pc);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, texwidth, texheight, 0, GL_RED, GL_UNSIGNED_BYTE, bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
}
glActiveTexture(GL_TEXTURE0);
try (var q = STBTTAlignedQuad.malloc()) {
float[] x = new float[1];
float[] y = new float[1];
glyphs = new Glyph[GLYPH_COUNT];
for (int i = 0; i < GLYPH_COUNT; i++) {
x[0] = 0f;
y[0] = fontSize;
stbtt_GetPackedQuad(packedchars, texwidth, texheight, i, x, y, q, true);
glyphs[i] = new Glyph((char) (i + 32), (int) (x[0] - 0f), new int[]{(int) q.x0(), (int) q.y0(), (int) q.x1(), (int) q.y1()}, new float[]{q.s0(), q.t0(), q.s1(), q.t1()});
}
}
}
}
int lineSpacing() {
return lineSpacing;
}
int textureNumber() {
return textureNumber;
}
int descent() {
return descent;
}
public int stringWidth(String text) {
var bytes = text.getBytes(StandardCharsets.US_ASCII);
int len = 0;
for (int i = 0; i < bytes.length; i++) {
final byte c = bytes[i];
len += switch (c) {
case '\n', '\t' -> 0;
case ' ' -> glyphs[0].charwidth();
default -> {
if (c - 32 < GLYPH_COUNT && c > 32) {
yield this.glyphs[c - 32].charwidth();
} else {
yield 0;
}
}
};
}
return len;
}
private record Pos(int x, int y, int minx) {}
/**
* A piece of text to display
*
* @param string The text
* @param colour The colour of the text as an RGBA packed int
*/
public record DisplayText(String string, int colour) {
private byte[] asBytes() {
return string.getBytes(StandardCharsets.US_ASCII);
}
Pos generateStringArray(SimpleFont font, Pos pos, SimpleBufferBuilder bb) {
for (int i = 0; i < asBytes().length; i++) {
byte c = asBytes()[i];
pos = switch (c) {
case '\n' -> new Pos(pos.minx(), pos.y()+font.lineSpacing(), pos.minx());
case '\t' -> new Pos(pos.x()+font.glyphs[0].charwidth() * 4, pos.y(), pos.minx());
case ' ' -> new Pos(pos.x()+font.glyphs[0].charwidth(), pos.y(), pos.minx());
default -> {
if (c-32 < GLYPH_COUNT && c > 32) {
pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb);
}
yield pos;
}
};
}
return pos;
}
}
/**
* Generate vertices for a set of display texts
* @param x The starting screen x coordinate
* @param y The starting screen y coordinate
* @param texts Some {@link DisplayText} to display
* @return a {@link SimpleBufferBuilder} that can draw the texts
*/
public SimpleBufferBuilder generateVerticesForTexts(int x, int y, SimpleBufferBuilder textBB, DisplayText... texts) {
var pos = new Pos(x, y, x);
for (DisplayText text : texts) {
pos = text.generateStringArray(this, pos, textBB);
}
return textBB;
}
}
@@ -1 +0,0 @@
net.minecraftforge.fml.earlydisplay.DisplayWindow
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,13 +0,0 @@
█▀▀▀▀▀█ ▀▄▄▄▀█ █ █▀▀▀▀▀█
█ ███ █ ███ ▀███▀ █ ███ █
█ ▀▀▀ █ ▀ ▄ ███▀█ █ ▀▀▀ █
▀▀▀▀▀▀▀ ▀ █ ▀ ▀▄█ ▀▀▀▀▀▀▀
█▀█▀▄▄▀▄▀ █▄▀▄ ▀▀▀ ▄▀▀▀▄▀
▄█▄▄ ▀▀███▀██▄ █▀ ▀▄▄
▀█▄▀ ▀▀▄▄▀▀ █▀█▄▄████ ▀▀█
▄▀▀▄▀ ▀▄▀▄█ ▀ ▀▀▀▄█ ▀▀▄
▀ ▀▀ ▄ ▄██▀ ▄█▀▀▀█▀█▀█
█▀▀▀▀▀█ ▄█▄▄▀▀▄█ ▀ █ ▀▀▀
█ ███ █ ▄▄ ▄ █ ▀██▀██▄██
█ ▀▀▀ █ ██▄███▀█ █ █ █▀
▀▀▀▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

+69 -120
View File
@@ -1,146 +1,95 @@
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
alias(libs.plugins.apt)
id 'com.github.ben-manes.versions'
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
id 'org.cadixdev.licenser'
}
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply plugin: 'org.javamodularity.moduleplugin'
apply from: rootProject.file('build_shared.gradle')
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
configurations.forEach{ it.transitive = false }
dependencies {
compileOnly(libs.jetbrains.annotations)
compileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
api(libs.bundles.asm) // Needed by all the black magic
api(libs.forgespi)
api(libs.mergetool.api)
api(libs.log4j.api)
api(libs.slf4j.api)
api(libs.guava)
api(libs.gson)
api(libs.maven.artifact)
api(libs.apache.commons)
api(libs.bundles.night.config)
api(libs.modlauncher)
api(libs.mojang.logging)
api(libs.jarjar.selector)
api(libs.jarjar.meta)
api("org.ow2.asm:asm:${ASM_VERSION}")
api("org.ow2.asm:asm-tree:${ASM_VERSION}")
api("org.ow2.asm:asm-commons:${ASM_VERSION}")
api("net.minecraftforge:forgespi:${SPI_VERSION}")
api("org.apache.logging.log4j:log4j-api:${LOG4J_VERSION}")
api("org.slf4j:slf4j-api:${SLF4J_API_VERSION}")
api("com.google.guava:guava:${GUAVA_VERSION}")
api("com.google.code.gson:gson:${GSON_VERSION}")
api("org.apache.maven:maven-artifact:${APACHE_MAVEN_ARTIFACT_VERSION}")
api("org.apache.commons:commons-lang3:${APACHE_COMMONS_LANG3_VERSION}")
api("com.electronwill.night-config:core:${NIGHTCONFIG_VERSION}")
api("com.electronwill.night-config:toml:${NIGHTCONFIG_VERSION}")
api("cpw.mods:modlauncher:${MODLAUNCHER_VERSION}")
api("net.minecraftforge:coremods:${COREMODS_VERSION}")
api("com.mojang:logging:${MOJANG_LOGGING_VERSION}")
api "net.minecraftforge:JarJarSelector:${JARJAR_VERSION}"
api "net.minecraftforge:JarJarMetadata:${JARJAR_VERSION}"
implementation(libs.jopt.simple)
implementation(libs.securemodules)
implementation(libs.accesstransformers)
implementation(libs.terminalconsoleappender)
implementation(libs.jimfs)
implementation(libs.roimfs)
implementation("net.sf.jopt-simple:jopt-simple:${JOPT_SIMPLE_VERSION}")
implementation("cpw.mods:securejarhandler:${SECUREJARHANDLER_VERSION}")
implementation("net.minecraftforge:accesstransformers:${ACCESSTRANSFORMERS_VERSION}")
implementation("net.minecrell:terminalconsoleappender:${TERMINALCONSOLEAPPENDER_VERSION}")
// implementation("org.jline:jline:${JLINE_VERSION}")
// Needed because we have a custom log4j plugin, and they removed package scanning and require a data file to be generated
implementation(libs.log4j.core)
annotationProcessor(libs.bundles.log4j)
implementation("org.apache.logging.log4j:log4j-core:${LOG4J_VERSION}")
annotationProcessor("org.apache.logging.log4j:log4j-core:${LOG4J_VERSION}")
testCompileOnly(libs.jetbrains.annotations)
testRuntimeOnly(libs.bootstrap)
testCompileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
testRuntimeOnly("cpw.mods:bootstraplauncher:${BOOTSTRAPLAUNCHER_VERSION}")
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
'Forge-Module-Layer': 'boot'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FMLLoader',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FMLLoader',
'Implementation-Vendor': 'Forge Development LLC',
'Implementation-Version': FORGE_VERSION
] as LinkedHashMap, 'net/minecraftforge/fml/loading/')
ext {
MANIFESTS = [
'/': [
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
'Git-Commit': GIT_INFO.abbreviatedId,
'Git-Branch': GIT_INFO.branch,
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}",
] as LinkedHashMap,
'net/minecraftforge/fml/loading/': [
'Specification-Title': 'FMLLoader',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML Loader',
'Implementation-Version': '1.0',
'Implementation-Vendor': 'Forge'
] as LinkedHashMap
]
}
jar.doFirst {
MANIFESTS.each { pkg, values ->
manifest.attributes(values, pkg)
}
}
tasks.withType(JavaCompile).configureEach {
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
include 'net/minecraftforge/'
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlloader'
pom {
name = project.name
description = 'Modifactions to Minecraft to enable mod developers.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
artifacts {
archives jar
archives sourcesJar
}
tasks.register('writeForgeVersionJson') {
doLast {
file('src/main/resources/forge_version.json').json = [
forge: FORGE_VERSION,
mc: MC_VERSION,
mcp: MCP_VERSION
]
}
}
tasks.register('jarJarOptionsJson', net.minecraftforge.forge.tasks.JarJarMetadataOptions) {
metadataFile = project.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) {
containedJarMetadata {
path = ''
}
resource = 'com/llamalad7/mixinextras/platform/forge/MixinExtrasConfigPlugin.class'
layer = 'GAMELIBRARY'
id = 'mixinextras'
nested = true
}
}
tasks.named('generateResources').configure {
dependsOn('eclipseJdt')
dependsOn('eclipseJdtApt')
dependsOn('eclipseFactorypath')
dependsOn('writeForgeVersionJson')
dependsOn('jarJarOptionsJson')
}
tasks.named('sourcesJar') {
dependsOn('jarJarOptionsJson')
}
eclipse {
classpath {
// We need to set the default output directory for the log4j annotation processor
// Ideally we'd just set the value, but the eclipse plugin deduplicates the output directories
// So we have to do a hacky whenMerged
//defaultOutputDir = file('bin/main')
file.whenMerged {
entries.each { entry ->
if (entry.kind == 'output' && entry.hasProperty('path'))
entry.path = 'bin/main'
}
}
}
}
publishing.publications.mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
}
@@ -34,10 +34,11 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
* }</code>
* </pre>
*/
public final class CapabilityTokenSubclass implements ILaunchPluginService {
private static final String FUNC_NAME = "getType";
private static final String FUNC_DESC = "()Ljava/lang/String;";
private static final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; // Don't directly reference this to prevent class loading.
public class CapabilityTokenSubclass implements ILaunchPluginService {
private final String FUNC_NAME = "getType";
private final String FUNC_DESC = "()Ljava/lang/String;";
private final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; //Don't directly reference this to prevent class loading.
@Override
public String name() {
@@ -48,48 +49,49 @@ public final class CapabilityTokenSubclass implements ILaunchPluginService {
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraft/") || internalName.startsWith("com/mojang/"))
return NAY;
if (internalName.startsWith("net/minecraftforge/common/capabilities/"))
return YAY;
return internalName.contains("$") ? YAY : NAY; // Anonymous subclasses only
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
}
@Override
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason) {
if (CAP_INJECT.equals(classNode.name)) {
for (MethodNode mtd : classNode.methods) {
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc)) {
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason)
{
if (CAP_INJECT.equals(classNode.name))
{
for (MethodNode mtd : classNode.methods)
{
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc))
{
mtd.access &= ~Opcodes.ACC_FINAL; // We have it final in code so people don't override it, cuz that'd be stupid, and make our transformer more complicated.
}
}
return ComputeFlags.SIMPLE_REWRITE;
} else if (CAP_INJECT.equals(classNode.superName)) {
var cls = new Object() { String value = null; };
}
else if (CAP_INJECT.equals(classNode.superName))
{
Holder cls = new Holder();
SignatureReader reader = new SignatureReader(classNode.signature); // Having a node version of this would probably be useful.
reader.accept(new SignatureVisitor(Opcodes.ASM9) {
final Deque<String> stack = new ArrayDeque<>();
reader.accept(new SignatureVisitor(Opcodes.ASM9)
{
Deque<String> stack = new ArrayDeque<>();
@Override
public void visitClassType(final String name) {
public void visitClassType(final String name)
{
stack.push(name);
}
@Override
public void visitInnerClassType(final String name) {
public void visitInnerClassType(final String name)
{
stack.push(stack.pop() + '$' + name);
}
@Override
public void visitEnd() {
public void visitEnd()
{
var val = stack.pop();
if (!stack.isEmpty() && CAP_INJECT.equals(stack.peek()))
cls.value = val;
@@ -104,8 +106,15 @@ public final class CapabilityTokenSubclass implements ILaunchPluginService {
mtd.visitInsn(Opcodes.ARETURN);
mtd.visitEnd();
return ComputeFlags.COMPUTE_MAXS;
} else {
}
else
{
return ComputeFlags.NO_REWRITE;
}
}
private static class Holder {
String value;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.common.asm;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
/**
* Removes the final modifier from fields with the @ObjectHolder annotation, prevents the JITer from in lining them so our runtime replacements can work.
* Will also de-finalize all fields in on class level annotations.
*/
public class ObjectHolderDefinalize implements ILaunchPluginService {
// Hardcoded map of vanilla classes that should have object holders for each field of the given registry type.
// IMPORTANT: Updates to this collection must be reflected in ObjectHolderRegistry. Duplicated cuz classloaders, yay!
// Classnames are validated in ObjectHolderRegistry.
private static final Map<String, VanillaObjectHolderData> VANILLA_OBJECT_HOLDERS = Stream.of(
new VanillaObjectHolderData("net.minecraft.world.level.block.Blocks", "block", "net.minecraft.world.level.block.Block"),
new VanillaObjectHolderData("net.minecraft.world.item.Items", "item", "net.minecraft.world.item.Item"),
new VanillaObjectHolderData("net.minecraft.world.item.enchantment.Enchantments", "enchantment", "net.minecraft.world.item.enchantment.Enchantment"),
new VanillaObjectHolderData("net.minecraft.world.effect.MobEffects", "mob_effect", "net.minecraft.world.effect.MobEffect"),
new VanillaObjectHolderData("net.minecraft.core.particles.ParticleTypes", "particle_type", "net.minecraft.core.particles.ParticleType"),
new VanillaObjectHolderData("net.minecraft.sounds.SoundEvents", "sound_event", "net.minecraft.sounds.SoundEvent")
).collect(Collectors.toMap(VanillaObjectHolderData::holderClass, Function.identity()));
private final String OBJECT_HOLDER = "Lnet/minecraftforge/registries/ObjectHolder;"; //Don't directly reference this to prevent class loading.
@Override
public String name() {
return "object_holder_definalize";
}
private static final EnumSet<Phase> YAY = EnumSet.of(Phase.AFTER);
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
}
private boolean hasHolder(List<AnnotationNode> lst)
{
return lst != null && lst.stream().anyMatch(n -> n.desc.equals(OBJECT_HOLDER));
}
private String getValue(List<AnnotationNode> lst)
{
AnnotationNode ann = lst.stream().filter(n -> n.desc.equals(OBJECT_HOLDER)).findFirst().get();
if (ann.values != null)
{
for (int x = 0; x < ann.values.size() - 1; x += 2) {
if (ann.values.get(x).equals("value")) {
return (String)ann.values.get(x + 1);
}
}
}
return null;
}
@Override
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason)
{
final AtomicBoolean changes = new AtomicBoolean();
//Must be public static finals, and non-array objects
final int flags = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL;
//Fix Annotated Fields before injecting from class level
classNode.fields.stream().filter(f -> ((f.access & flags) == flags) && f.desc.startsWith("L") && hasHolder(f.visibleAnnotations)).forEach(f ->
{
int prev = f.access;
f.access &= ~Opcodes.ACC_FINAL; //Strip final
f.access |= Opcodes.ACC_SYNTHETIC; //Add Synthetic so we can check in runtime. ? Good idea?
changes.compareAndSet(false, prev != f.access);
});
if (VANILLA_OBJECT_HOLDERS.containsKey(classType.getClassName())) //Class level, de-finalize all fields and add @ObjectHolder to them!
{
classNode.fields.stream().filter(f -> ((f.access & flags) == flags) && f.desc.startsWith("L")).forEach(f ->
{
int prev = f.access;
f.access &= ~Opcodes.ACC_FINAL;
f.access |= Opcodes.ACC_SYNTHETIC;
/*if (!hasHolder(f.visibleAnnotations)) //Add field level annotation, doesn't do anything until after we figure out how ASMDataTable is gatherered
{
if (value == null)
f.visitAnnotation(OBJECT_HOLDER, true);
else
f.visitAnnotation(OBJECT_HOLDER, true).visit("value", value + ":" + f.name.toLowerCase());
}*/
changes.compareAndSet(false, prev != f.access);
});
}
return changes.get() ? ComputeFlags.SIMPLE_REWRITE : ComputeFlags.NO_REWRITE;
}
private record VanillaObjectHolderData(String holderClass, String registryName, String registryType) {}
}
@@ -7,9 +7,9 @@ package net.minecraftforge.fml.common.asm;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.loading.LoadingModList;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@@ -20,6 +20,8 @@ import org.objectweb.asm.tree.MethodNode;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
/**
* Modifies specified enums to allow runtime extension by making the $VALUES field non-final and
@@ -28,16 +30,15 @@ import org.slf4j.Logger;
public class RuntimeEnumExtender implements ILaunchPluginService {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Type STRING = Type.getType(String.class);
private static final Type ENUM = Type.getType(Enum.class);
private static final Type MARKER_IFACE = Type.getType("Lnet/minecraftforge/common/IExtensibleEnum;");
private static final Type ARRAY_UTILS = Type.getType("Lorg/apache/commons/lang3/ArrayUtils;"); //Don't directly reference this to prevent class loading.
private static final String ADD_DESC = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(Object[].class), Type.getType(Object.class));
private static final Type UNSAFE_HACKS = Type.getType("Lnet/minecraftforge/fml/unsafe/UnsafeHacks;"); //Again, not direct reference to prevent class loading.
private static final String CLEAN_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Class.class));
private static final String NAME_DESC = Type.getMethodDescriptor(STRING);
private static final String EQUALS_DESC = Type.getMethodDescriptor(Type.BOOLEAN_TYPE, STRING);
private static final int FLAGS = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC;
private final Type STRING = Type.getType(String.class);
private final Type ENUM = Type.getType(Enum.class);
private final Type MARKER_IFACE = Type.getType("Lnet/minecraftforge/common/IExtensibleEnum;");
private final Type ARRAY_UTILS = Type.getType("Lorg/apache/commons/lang3/ArrayUtils;"); //Don't directly reference this to prevent class loading.
private final String ADD_DESC = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(Object[].class), Type.getType(Object.class));
private final Type UNSAFE_HACKS = Type.getType("Lnet/minecraftforge/fml/unsafe/UnsafeHacks;"); //Again, not direct reference to prevent class loading.
private final String CLEAN_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Class.class));
private final String NAME_DESC = Type.getMethodDescriptor(STRING);
private final String EQUALS_DESC = Type.getMethodDescriptor(Type.BOOLEAN_TYPE, STRING);
@Override
public String name() {
@@ -47,21 +48,10 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
private static final EnumSet<Phase> YAY = EnumSet.of(Phase.AFTER);
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
private static final class LazyInit {
private LazyInit() {}
private static final boolean ANY_MODS = LoadingModList.getMods().size() > 2; // 2: forge, minecraft
}
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty || !LazyInit.ANY_MODS)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraft/client/model") || internalName.startsWith("net/minecraft/world"))
return YAY;
return NAY;
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
}
@Override
@@ -70,24 +60,25 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
if ((classNode.access & Opcodes.ACC_ENUM) == 0)
return ComputeFlags.NO_REWRITE;
if (!classNode.interfaces.contains(MARKER_IFACE.getInternalName()))
return ComputeFlags.NO_REWRITE;
Type array = Type.getType("[" + classType.getDescriptor());
String arrayDesc = array.getDescriptor();
final int flags = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC;
FieldNode values = classNode.fields.stream().filter(f -> f.desc.equals(arrayDesc) && ((f.access & FLAGS) == FLAGS)).findFirst().orElse(null);
FieldNode values = classNode.fields.stream().filter(f -> f.desc.contentEquals(array.getDescriptor()) && ((f.access & flags) == flags)).findFirst().orElse(null);
if (!classNode.interfaces.contains(MARKER_IFACE.getInternalName())) {
return ComputeFlags.NO_REWRITE;
}
//Static methods named "create" with first argument as a string
List<MethodNode> candidates = classNode.methods.stream()
.filter(m -> ((m.access & Opcodes.ACC_STATIC) != 0) && m.name.equals("create"))
.toList();
.collect(Collectors.toList());
if (candidates.isEmpty()) {
throw new IllegalStateException("IExtensibleEnum has no candidate factory methods: " + classType.getClassName());
}
for (var mtd : candidates)
candidates.forEach(mtd ->
{
Type[] args = Type.getArgumentTypes(mtd.desc);
if (args.length == 0 || !args[0].equals(STRING)) {
@@ -119,7 +110,8 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
Type[] ctrArgs = new Type[args.length + 1];
ctrArgs[0] = STRING;
ctrArgs[1] = Type.INT_TYPE;
System.arraycopy(args, 1, ctrArgs, 2, args.length - 1);
for (int x = 1; x < args.length; x++)
ctrArgs[1 + x] = args[x];
String desc = Type.getMethodDescriptor(Type.VOID_TYPE, ctrArgs);
@@ -241,7 +233,7 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
ins.load(vars, classType);
ins.areturn(classType);
}
}
});
return ComputeFlags.COMPUTE_FRAMES;
}
@@ -6,14 +6,14 @@
package net.minecraftforge.fml.loading;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class BackgroundWaiter {
private static ExecutorService runner = Executors.newSingleThreadExecutor();
public static void runAndTick(Runnable r, Runnable tick) {
var runner = Executors.newSingleThreadExecutor();
ImmediateWindowHandler.updateProgress("Loading bootstrap resources");
final Future<?> work = runner.submit(r);
do {
tick.run();
@@ -0,0 +1,61 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading;
import cpw.mods.modlauncher.api.NamedPath;
import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService;
import org.apache.logging.log4j.LogManager;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class ClasspathTransformerDiscoverer implements ITransformerDiscoveryService {
private final List<Path> legacyClasspath = Arrays.stream(System.getProperty("legacyClassPath", "").split(File.pathSeparator)).map(Path::of).toList();
@Override
public List<NamedPath> candidates(Path gameDirectory) {
return Collections.emptyList();
}
@Override
public List<NamedPath> candidates(final Path gameDirectory, final String launchTarget) {
if (launchTarget != null && launchTarget.contains("dev")) {
this.scan(gameDirectory);
}
return List.copyOf(found);
}
private final static List<NamedPath> found = new ArrayList<>();
public static List<Path> allExcluded() {
return found.stream().map(np->np.paths()[0]).toList();
}
private void scan(final Path gameDirectory) {
try {
locateTransformers("META-INF/services/cpw.mods.modlauncher.api.ITransformationService");
locateTransformers("META-INF/services/net.minecraftforge.forgespi.locating.IModLocator");
} catch (IOException e) {
LogManager.getLogger().error("Error during discovery of transform services from the classpath", e);
}
}
private void locateTransformers(String resource) throws IOException {
final Enumeration<URL> resources = ClassLoader.getSystemClassLoader().getResources(resource);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
Path path = ClasspathLocatorUtils.findJarPathFor(resource, url.toString(), url);
if (legacyClasspath.stream().anyMatch(path::equals) || !Files.exists(path) || Files.isDirectory(path))
continue;
found.add(new NamedPath(path.toUri().toString(), path));
}
}
}
@@ -14,8 +14,6 @@ import java.util.List;
* or server.
*/
public class EarlyLoadingException extends RuntimeException {
private static final long serialVersionUID = 2302192216172508006L;
public static class ExceptionData {
private final IModInfo modInfo;
private final String i18message;
@@ -5,7 +5,6 @@
package net.minecraftforge.fml.loading;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.ConfigSpec;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
@@ -16,79 +15,21 @@ import org.slf4j.Logger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
public class FMLConfig
{
public enum ConfigValue {
EARLY_WINDOW_CONTROL("earlyWindowControl", Boolean.TRUE, "Should we control the window. Disabling this disables new GL features and can be bad for mods that rely on them."),
MAX_THREADS("maxThreads", -1, "Max threads for early initialization parallelism, -1 is based on processor count", FMLConfig::maxThreads),
VERSION_CHECK("versionCheck", Boolean.TRUE, "Enable forge global version checking"),
DEFAULT_CONFIG_PATH("defaultConfigPath", "defaultconfigs", "Default config path for servers"),
DISABLE_OPTIMIZED_DFU("disableOptimizedDFU", Boolean.TRUE, "Disables Optimized DFU client-side - already disabled on servers"),
EARLY_WINDOW_PROVIDER("earlyWindowProvider", "fmlearlywindow", "Early window provider"),
EARLY_WINDOW_WIDTH("earlyWindowWidth", 854, "Early window width"),
EARLY_WINDOW_HEIGHT("earlyWindowHeight", 480, "Early window height"),
EARLY_WINDOW_FBSCALE("earlyWindowFBScale", 1, "Early window framebuffer scale"),
EARLY_WINDOW_MAXIMIZED("earlyWindowMaximized", Boolean.FALSE, "Early window starts maximized"),
EARLY_WINDOW_SKIP_GL_VERSIONS("earlyWindowSkipGLVersions", List.of(), "Skip specific GL versions, may help with buggy graphics card drivers"),
EARLY_WINDOW_SQUIR("earlyWindowSquir", Boolean.FALSE, "Squir?"),
EARLY_WINDOW_SHOW_CPU("earlyWindowShowCPU", Boolean.FALSE, "Whether to show CPU usage stats in early window"),
EARLY_WINDOW_LOG_HELP_MSG("earlyWindowLogHelpMessage", Boolean.TRUE, "Whether to log a help message on first attempt, to aid troubleshooting. This setting should automatically disable itself after a successful launch"),
;
private final String entry;
private final Object defaultValue;
private final String comment;
private final Function<Object, Object> entryFunction;
ConfigValue(final String entry, final Object defaultValue, final String comment) {
this(entry, defaultValue, comment, Function.identity());
}
ConfigValue(final String entry, final Object defaultValue, final String comment, Function<Object, Object> entryFunction) {
this.entry = entry;
this.defaultValue = defaultValue;
this.comment = comment;
this.entryFunction = entryFunction;
}
void buildConfigEntry(ConfigSpec spec, CommentedConfig commentedConfig) {
if (this.defaultValue instanceof List<?> list) {
spec.defineList(this.entry, list, e -> e instanceof String);
} else {
spec.define(this.entry, this.defaultValue);
}
commentedConfig.add(this.entry, this.defaultValue);
commentedConfig.setComment(this.entry, this.comment);
}
@SuppressWarnings("unchecked")
private <T> T getConfigValue(CommentedFileConfig config) {
return (T) this.entryFunction.apply(config.get(this.entry));
}
public <T> void updateValue(final CommentedFileConfig configData, final T value) {
configData.set(this.entry, value);
}
}
private static Object maxThreads(final Object value) {
int val = (Integer)value;
if (val <= 0) return Runtime.getRuntime().availableProcessors();
else return val;
}
private static final Logger LOGGER = LogUtils.getLogger();
private static final FMLConfig INSTANCE = new FMLConfig();
private static final ConfigSpec configSpec = new ConfigSpec();
private static final CommentedConfig configComments = CommentedConfig.inMemory();
private static FMLConfig INSTANCE = new FMLConfig();
private static ConfigSpec configSpec = new ConfigSpec();
static {
for (ConfigValue cv: ConfigValue.values()) {
cv.buildConfigEntry(configSpec, configComments);
}
configSpec.define("splashscreen", Boolean.TRUE);
configSpec.define("maxThreads", -1);
configSpec.define("versionCheck", Boolean.TRUE);
configSpec.define("defaultConfigPath", "defaultconfigs");
configSpec.define("disableOptimizedDFU", Boolean.TRUE);
}
private CommentedFileConfig configData;
@@ -97,6 +38,7 @@ public class FMLConfig
{
configData = CommentedFileConfig.builder(configFile).sync()
.onFileNotFound(FileNotFoundAction.copyData(Objects.requireNonNull(getClass().getResourceAsStream("/META-INF/defaultfmlconfig.toml"))))
.autosave().autoreload()
.writingMode(WritingMode.REPLACE)
.build();
try
@@ -105,14 +47,13 @@ public class FMLConfig
}
catch (ParsingException e)
{
throw new RuntimeException("Failed to load FML config from " + configFile, e);
throw new RuntimeException("Failed to load FML config from " + configFile.toString(), e);
}
if (!configSpec.isCorrect(configData)) {
LOGGER.warn(CORE, "Configuration file {} is not correct. Correcting", configFile);
configSpec.correct(configData, (action, path, incorrectValue, correctedValue) ->
LOGGER.info(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue));
LOGGER.warn(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue));
}
configData.putAllComments(configComments);
configData.save();
}
@@ -123,34 +64,33 @@ public class FMLConfig
if (LOGGER.isTraceEnabled(CORE))
{
LOGGER.trace(CORE, "Loaded FML config from {}", FMLPaths.FMLCONFIG.get());
for (ConfigValue cv: ConfigValue.values()) {
LOGGER.trace(CORE, "FMLConfig {} is {}", cv.entry, cv.getConfigValue(INSTANCE.configData));
}
LOGGER.trace(CORE, "Splash screen is {}", FMLConfig.splashScreenEnabled());
LOGGER.trace(CORE, "Max threads for mod loading computed at {}", FMLConfig.loadingThreadCount());
LOGGER.trace(CORE, "Version check is {}", FMLConfig.runVersionCheck());
LOGGER.trace(CORE, "Default config paths at {}", FMLConfig.defaultConfigPath());
}
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH)));
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.defaultConfigPath()));
}
public static String getConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
public static boolean splashScreenEnabled() {
return INSTANCE.configData.<Boolean>getOptional("splashscreen").orElse(Boolean.FALSE);
}
public static boolean getBoolConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
public static int loadingThreadCount() {
int val = INSTANCE.configData.<Integer>getOptional("maxThreads").orElse(-1);
if (val <= 0) return Runtime.getRuntime().availableProcessors();
return val;
}
public static int getIntConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static <A> List<A> getListConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static <T> void updateConfig(ConfigValue v, T value) {
v.updateValue(INSTANCE.configData, value);
INSTANCE.configData.save();
public static boolean runVersionCheck() {
return INSTANCE.configData.<Boolean>getOptional("versionCheck").orElse(Boolean.TRUE);
}
public static String defaultConfigPath() {
return getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH);
return INSTANCE.configData.<String>getOptional("defaultConfigPath").orElse("defaultconfigs");
}
public static boolean isOptimizedDFUDisabled() {
return INSTANCE.configData.<Boolean>getOptional("disableOptimizedDFU").orElse(Boolean.TRUE);
}
}
@@ -8,36 +8,45 @@ package net.minecraftforge.fml.loading;
import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.api.*;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
import net.minecraftforge.fml.loading.moddiscovery.BackgroundScanHandler;
import net.minecraftforge.fml.loading.moddiscovery.ModDiscoverer;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModValidator;
import net.minecraftforge.accesstransformer.service.AccessTransformerService;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.loading.progress.EarlyProgressVisualization;
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
import net.minecraftforge.fml.loading.targets.CommonLaunchHandler;
import net.minecraftforge.forgespi.Environment;
import net.minecraftforge.forgespi.coremod.ICoreModProvider;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
import static net.minecraftforge.fml.loading.LogMarkers.SCAN;
public final class FMLLoader {
public class FMLLoader
{
private static final Logger LOGGER = LogUtils.getLogger();
private static AccessTransformerService accessTransformer;
private static ModDiscoverer modDiscoverer;
private static ICoreModProvider coreModProvider;
private static ILaunchPluginService eventBus;
private static LanguageLoadingProvider languageLoadingProvider;
private static Dist dist;
private static String naming;
private static LoadingModList loadingModList;
private static RuntimeDistCleaner runtimeDistCleaner;
private static Path gamePath;
private static final VersionInfo versionInfo = VersionInfo.detect();
private static VersionInfo versionInfo;
private static String launchHandlerName;
private static CommonLaunchHandler commonLaunchHandler;
public static Runnable progressWindowTick;
@@ -46,73 +55,80 @@ public final class FMLLoader {
private static boolean production;
private static IModuleLayerManager moduleLayerManager;
static void onInitialLoad(IEnvironment env, Set<String> otherServices) throws IncompatibleEnvironmentException {
LOGGER.debug(CORE, "Detected version data : {}", versionInfo);
LOGGER.debug(CORE, "FML {} loading", LauncherVersion.getVersion());
static void onInitialLoad(IEnvironment environment, Set<String> otherServices) throws IncompatibleEnvironmentException
{
final String version = LauncherVersion.getVersion();
LOGGER.debug(CORE,"FML {} loading", version);
final Package modLauncherPackage = ITransformationService.class.getPackage();
LOGGER.debug(CORE,"FML found ModLauncher version : {}", modLauncherPackage.getImplementationVersion());
if (!modLauncherPackage.isCompatibleWith("4.0")) {
LOGGER.error(CORE, "Found incompatible ModLauncher specification : {}, version {} from {}", modLauncherPackage.getSpecificationVersion(), modLauncherPackage.getImplementationVersion(), modLauncherPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible modlauncher found "+modLauncherPackage.getSpecificationVersion());
}
checkPackage(ITransformationService.class, "4.0", "ModLauncher");
accessTransformer = getPlugin(env, "accesstransformer", "1.0", "AccessTransformer");
runtimeDistCleaner = getPlugin(env, "runtimedistcleaner", "1.0", "RuntimeDistCleaner");
checkPackage(Environment.class, "2.0", "ForgeSPI");
accessTransformer = (AccessTransformerService) environment.findLaunchPlugin("accesstransformer").orElseThrow(()-> {
LOGGER.error(CORE, "Access Transformer library is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing AccessTransformer, cannot run");
});
final Package atPackage = accessTransformer.getClass().getPackage();
LOGGER.debug(CORE,"FML found AccessTransformer version : {}", atPackage.getImplementationVersion());
if (!atPackage.isCompatibleWith("1.0")) {
LOGGER.error(CORE, "Found incompatible AccessTransformer specification : {}, version {} from {}", atPackage.getSpecificationVersion(), atPackage.getImplementationVersion(), atPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible accesstransformer found "+atPackage.getSpecificationVersion());
}
eventBus = environment.findLaunchPlugin("eventbus").orElseThrow(()-> {
LOGGER.error(CORE, "Event Bus library is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing EventBus, cannot run");
});
final Package eventBusPackage = eventBus.getClass().getPackage();
LOGGER.debug(CORE,"FML found EventBus version : {}", eventBusPackage.getImplementationVersion());
if (!eventBusPackage.isCompatibleWith("1.0")) {
LOGGER.error(CORE, "Found incompatible EventBus specification : {}, version {} from {}", eventBusPackage.getSpecificationVersion(), eventBusPackage.getImplementationVersion(), eventBusPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible eventbus found "+eventBusPackage.getSpecificationVersion());
}
runtimeDistCleaner = (RuntimeDistCleaner)environment.findLaunchPlugin("runtimedistcleaner").orElseThrow(()-> {
LOGGER.error(CORE, "Dist Cleaner is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing DistCleaner, cannot run!");
});
LOGGER.debug(CORE, "Found Runtime Dist Cleaner");
var coreModProviders = ServiceLoaderUtils.streamWithErrorHandling(ServiceLoader.load(FMLLoader.class.getModule().getLayer(), ICoreModProvider.class), sce -> LOGGER.error(CORE, "Failed to load a coremod library, expect problems", sce)).toList();
if (coreModProviders.isEmpty()) {
LOGGER.error(CORE, "Found no coremod provider. Cannot run");
throw new IncompatibleEnvironmentException("No coremod library found");
} else if (coreModProviders.size() > 1) {
LOGGER.error(CORE, "Found multiple coremod providers : {}. Cannot run", coreModProviders.stream().map(p -> p.getClass().getName()).collect(Collectors.toList()));
throw new IncompatibleEnvironmentException("Multiple coremod libraries found");
}
coreModProvider = coreModProviders.get(0);
final Package coremodPackage = coreModProvider.getClass().getPackage();
LOGGER.debug(CORE,"FML found CoreMod version : {}", coremodPackage.getImplementationVersion());
LOGGER.debug(CORE, "Found ForgeSPI package implementation version {}", Environment.class.getPackage().getImplementationVersion());
LOGGER.debug(CORE, "Found ForgeSPI package specification {}", Environment.class.getPackage().getSpecificationVersion());
if (Integer.parseInt(Environment.class.getPackage().getSpecificationVersion()) < 2) {
LOGGER.error(CORE, "Found an out of date ForgeSPI implementation: {}, loading cannot continue", Environment.class.getPackage().getSpecificationVersion());
throw new IncompatibleEnvironmentException("ForgeSPI is out of date, we cannot continue");
}
try {
Class.forName("com.electronwill.nightconfig.core.Config", false, env.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, env.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.core.Config", false, environment.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, environment.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
LOGGER.error(CORE, "Failed to load NightConfig");
throw new IncompatibleEnvironmentException("Missing NightConfig");
}
}
@SuppressWarnings("unchecked")
private static <T> T getPlugin(IEnvironment env, String id, String version, String name) throws IncompatibleEnvironmentException {
var plugin = env.findLaunchPlugin(id).orElse(null);
if (plugin == null) {
LOGGER.error(CORE, "{} library is missing, we need this to run", name);
throw new IncompatibleEnvironmentException("Missing " + name + ", cannot run");
}
checkPackage(plugin.getClass(), version, name);
return (T) plugin;
}
private static void checkPackage(Class<?> cls, String version, String name) throws IncompatibleEnvironmentException {
var pkg = cls.getPackage();
var info = JarVersionLookupHandler.getInfo(pkg);
LOGGER.debug(CORE, "Found {} version: {}", name, info.impl().version().orElse("MISSING"));
if (!pkg.isCompatibleWith(version)) {
LOGGER.error(CORE, "Found incompatible {} specification: {}, version {} from {}", name,
info.spec().version().orElse("MISSING"),
info.impl().version().orElse("MISSING"),
info.impl().vendor().orElse("MISSING")
);
throw new IncompatibleEnvironmentException("Incompatible " + name + " found " + info.spec().version().orElse("MISSING"));
}
}
@SuppressWarnings("unused")
private static <T> T getSingleService(Class<T> clazz, String name) throws IncompatibleEnvironmentException {
var providers = new ArrayList<T>();
for (var itr = ServiceLoader.load(FMLLoader.class.getModule().getLayer(), clazz).iterator(); itr.hasNext(); ) {
try {
providers.add(itr.next());
} catch (ServiceConfigurationError e) {
LOGGER.error(CORE, "Failed to load a " + name + " library, expect problems", e);
}
}
if (providers.isEmpty()) {
LOGGER.error(CORE, "Found no {} provider. Cannot run", name);
throw new IncompatibleEnvironmentException("No " + name + " library found");
} else if (providers.size() > 1) {
LOGGER.error(CORE, "Found multiple {} providers: {}. Cannot run", name, providers.stream().map(p -> p.getClass().getName()).toList());
throw new IncompatibleEnvironmentException("Multiple " + name + " libraries found");
}
return providers.get(0);
}
static void setupLaunchHandler(final IEnvironment environment, final Map<String, Object> arguments) {
static void setupLaunchHandler(final IEnvironment environment, final Map<String, Object> arguments)
{
final String launchTarget = environment.getProperty(IEnvironment.Keys.LAUNCHTARGET.get()).orElse("MISSING");
arguments.put("launchTarget", launchTarget);
final Optional<ILaunchHandlerService> launchHandler = environment.findLaunchHandler(launchTarget);
@@ -122,58 +138,86 @@ public final class FMLLoader {
throw new RuntimeException("Missing launch handler: " + launchTarget);
}
// TODO: [FML][Loader] What the fuck is the point of using a service if you require a specific concrete class
if (!(launchHandler.get() instanceof CommonLaunchHandler)) {
LOGGER.error(CORE, "Incompatible Launch handler found - type {}, cannot continue", launchHandler.get().getClass().getName());
throw new RuntimeException("Incompatible launch handler found");
}
commonLaunchHandler = (CommonLaunchHandler)launchHandler.get();
launchHandlerName = launchHandler.get().name();
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Path.of(".").toAbsolutePath());
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Paths.get(".").toAbsolutePath());
naming = commonLaunchHandler.getNaming();
dist = commonLaunchHandler.getDist();
production = commonLaunchHandler.isProduction();
accessTransformer.getExtension().accept(Map.entry(naming, "srg"));
versionInfo = new VersionInfo(arguments);
StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Early Loading!"));
accessTransformer.getExtension().accept(Pair.of(naming, "srg"));
LOGGER.debug(CORE,"Received command line version data : {}", versionInfo);
runtimeDistCleaner.getExtension().accept(dist);
}
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments) {
LOGGER.debug(SCAN, "Scanning for Mod Locators");
modValidator = ModDiscoverer.discoverMods(arguments);
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments)
{
LOGGER.debug(SCAN,"Scanning for Mod Locators");
modDiscoverer = new ModDiscoverer(arguments);
modValidator = modDiscoverer.discoverMods();
var pluginResources = modValidator.getPluginResources();
return List.of(pluginResources);
}
public static List<ITransformationService.Resource> completeScan(IModuleLayerManager layerManager) {
progressWindowTick = EarlyProgressVisualization.INSTANCE.accept(dist, commonLaunchHandler.isData(), versionInfo.mcVersion());
moduleLayerManager = layerManager;
LanguageLoadingProvider.reload();
languageLoadingProvider = new LanguageLoadingProvider();
backgroundScanHandler = modValidator.stage2Validation();
loadingModList = backgroundScanHandler.getLoadingModList();
return List.of(modValidator.getModResources());
}
public static ICoreModProvider getCoreModProvider() {
return coreModProvider;
}
public static LanguageLoadingProvider getLanguageLoadingProvider()
{
return languageLoadingProvider;
}
static ModDiscoverer getModDiscoverer() {
return modDiscoverer;
}
public static CommonLaunchHandler getLaunchHandler() {
return commonLaunchHandler;
}
public static void addAccessTransformer(Path atPath, ModFile modName) {
LOGGER.debug(SCAN, "Adding Access Transformer {} in {}", atPath, modName.getFilePath());
public static void addAccessTransformer(Path atPath, ModFile modName)
{
LOGGER.debug(SCAN, "Adding Access Transformer in {}", modName.getFilePath());
accessTransformer.offerResource(atPath, modName.getFileName());
}
public static Dist getDist() {
public static Dist getDist()
{
return dist;
}
public static void beforeStart(ModuleLayer gameLayer) {
ImmediateWindowHandler.acceptGameLayer(gameLayer);
ImmediateWindowHandler.updateProgress("Launching minecraft");
public static void beforeStart(ClassLoader launchClassLoader)
{
StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Launching minecraft"));
progressWindowTick.run();
}
public static Path getGamePath() {
public static LoadingModList getLoadingModList()
{
return loadingModList;
}
public static Path getGamePath()
{
return gamePath;
}
@@ -190,7 +234,7 @@ public final class FMLLoader {
}
public static List<Map<String, String>> modLauncherModList() {
return Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElse(List.of());
return Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElseGet(Collections::emptyList);
}
public static String launcherHandlerName() {
@@ -74,6 +74,14 @@ public enum FMLPaths
}
}
/**
* @deprecated Use {@link #getOrCreateGameRelativePath(Path)} instead.
*/
@Deprecated(forRemoval = true, since = "1.19.4")
public static Path getOrCreateGameRelativePath(Path path, String name) {
return getOrCreateGameRelativePath(path);
}
public static Path getOrCreateGameRelativePath(Path path) {
Path gameFolderPath = FMLPaths.GAMEDIR.get().resolve(path);
Loaded 100 of 3805 files, more files were not shown because too many files have changed in this diff. Show more