mirror of
https://github.com/MinecraftForge/MinecraftForge.git
synced 2026-11-14 09:05:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3da1b644d0 | ||
|
|
d3476f3f80 | ||
|
|
6665cdcf4e | ||
|
|
d9665f6447 | ||
|
|
ec73fd5240 | ||
|
|
3bbdbacb81 | ||
|
|
478d8b3f18 | ||
|
|
03f928a3de | ||
|
|
4834f16aef | ||
|
|
7846d25606 | ||
|
|
77cb0c5c4e | ||
|
|
5e421e8b8b | ||
|
|
5f37e8af53 | ||
|
|
5ab792f1d7 | ||
|
|
38ebbf189c | ||
|
|
f247eebd55 | ||
|
|
555989cf10 | ||
|
|
49fe93b1b5 | ||
|
|
48167e93b4 | ||
|
|
2bd132a6f8 | ||
|
|
b8168a12df | ||
|
|
df271e0265 | ||
|
|
d7687a72d5 | ||
|
|
047e5fe252 | ||
|
|
5ac2675f87 | ||
|
|
b544849f7d | ||
|
|
39683f28dd | ||
|
|
da4fd10ee8 | ||
|
|
e1c2add370 | ||
|
|
c71ba862cf | ||
|
|
3c057d277b | ||
|
|
b99abff6ce | ||
|
|
558e7d754e | ||
|
|
d29524bac3 | ||
|
|
929064ee69 | ||
|
|
b4024a7884 | ||
|
|
07f6499143 | ||
|
|
236842f210 | ||
|
|
ef811aebe8 | ||
|
|
d61de63482 | ||
|
|
8368a1d20a | ||
|
|
8b8d3fdef7 | ||
|
|
f5d4fbbee5 | ||
|
|
6f4f60e213 | ||
|
|
01bfc33b28 | ||
|
|
17e1002e86 | ||
|
|
da1b8f4efa | ||
|
|
42bbe44a34 | ||
|
|
f095474adf | ||
|
|
1b5ca64f20 | ||
|
|
b518c804fb | ||
|
|
e50a0ffc6c | ||
|
|
7ea0a41fe2 | ||
|
|
f3d16621c0 | ||
|
|
aa55a6a831 | ||
|
|
3449217c65 | ||
|
|
41609a03d2 | ||
|
|
9b8e5c7109 | ||
|
|
f05ad3afd0 | ||
|
|
6a615f1a4b | ||
|
|
4376cd6b85 | ||
|
|
d17a111dc0 | ||
|
|
1b99b2cea4 | ||
|
|
4ef6695a30 | ||
|
|
0e6341a23e | ||
|
|
17ce723edf | ||
|
|
fc80e30289 | ||
|
|
ecd1491f96 | ||
|
|
8f7fbc76e1 | ||
|
|
ec405958c9 | ||
|
|
e2ff990601 | ||
|
|
8443262e39 | ||
|
|
8e09d47cd6 | ||
|
|
0404d2d776 | ||
|
|
aea804cb6e | ||
|
|
0bffcb83c5 | ||
|
|
9a022d1451 | ||
|
|
58aa3b276c | ||
|
|
b0ccf1a480 | ||
|
|
c1d703e395 | ||
|
|
728fd6ff24 | ||
|
|
0a885a77e7 | ||
|
|
15dbadaae6 | ||
|
|
4c1c3aafdb | ||
|
|
f06727a205 | ||
|
|
ef0e7bc17f | ||
|
|
ec7a098f81 | ||
|
|
ff25961a05 | ||
|
|
c999a2d4eb | ||
|
|
ce79df9716 | ||
|
|
eae9b9145d | ||
|
|
7f56986027 | ||
|
|
c6874bce67 | ||
|
|
aa11175f41 | ||
|
|
2730559ff3 | ||
|
|
e80c3402b7 | ||
|
|
837c259668 | ||
|
|
2e48910b4c | ||
|
|
70dd556db6 | ||
|
|
e44a558f37 | ||
|
|
1e154cbe3e | ||
|
|
d5c5df1b64 | ||
|
|
5e75de5eb7 | ||
|
|
ddb1e52e99 | ||
|
|
9e55363e2c | ||
|
|
219abfed22 | ||
|
|
6915753b5a | ||
|
|
1527738ddc | ||
|
|
18c806f1d0 | ||
|
|
47f7c68ed5 | ||
|
|
5e8942e0a7 | ||
|
|
a02f3f9841 | ||
|
|
c5e462f7d2 | ||
|
|
d1f79d6655 | ||
|
|
9d5e81b1a9 | ||
|
|
a1c275df46 | ||
|
|
4ef7fe8bd8 | ||
|
|
ce8838e721 | ||
|
|
b7e2593d48 | ||
|
|
33d177f02e | ||
|
|
5d2186df63 | ||
|
|
77af36b807 | ||
|
|
54eeca0aed | ||
|
|
3de0566425 | ||
|
|
a5ebf3b645 | ||
|
|
63d0496cc4 | ||
|
|
31f623e794 | ||
|
|
eb001a6382 | ||
|
|
504dcad849 | ||
|
|
81f7f029c3 | ||
|
|
207c1997c9 | ||
|
|
e348ae5b84 | ||
|
|
0f3b8ee830 | ||
|
|
b4b6e48a7a | ||
|
|
d50a3b5ab7 | ||
|
|
88904a1d09 | ||
|
|
b864faf0ef | ||
|
|
c97eabc0c4 | ||
|
|
6ae39c8597 | ||
|
|
dcafb4f633 | ||
|
|
1ea7ce9831 | ||
|
|
674f2efd54 | ||
|
|
02e82c672d | ||
|
|
b25f6e3a7c | ||
|
|
e69b07a54a | ||
|
|
79efe2ac36 | ||
|
|
464483db5b | ||
|
|
f49adb285f | ||
|
|
55862dfd1d | ||
|
|
124b9bd8d3 | ||
|
|
bae3915728 | ||
|
|
b3c2c62c7e | ||
|
|
68698d70c1 | ||
|
|
d55dee9394 | ||
|
|
2d114f2719 | ||
|
|
3e6583af01 | ||
|
|
762f6a7d78 | ||
|
|
3c5a534b7a | ||
|
|
bc62d1693a | ||
|
|
554ead8d4a | ||
|
|
ec5ac91389 | ||
|
|
f63813d929 | ||
|
|
5d4825ac4b | ||
|
|
3024ef7707 | ||
|
|
0d3b488728 | ||
|
|
a01012f20c | ||
|
|
d4e3ef4a22 | ||
|
|
d7293a78dc | ||
|
|
8e4e02e258 | ||
|
|
2f5a6fee48 | ||
|
|
0e74886f8d | ||
|
|
07977ba242 | ||
|
|
a894c244fe | ||
|
|
cf32a799a4 | ||
|
|
0fda1d9393 | ||
|
|
c7f0acbe38 | ||
|
|
9c06c20023 | ||
|
|
041084d1da | ||
|
|
e643d0a11f | ||
|
|
5efb8d3f12 | ||
|
|
70537542d1 | ||
|
|
bd4b059c2b | ||
|
|
e0d2c68c5b | ||
|
|
86d4bda32e | ||
|
|
98b90f26bc | ||
|
|
4e7c471fa3 | ||
|
|
ec6f9fc6e7 | ||
|
|
427fef1d4b | ||
|
|
83594b0b0a | ||
|
|
b76064e57a | ||
|
|
b2bf70c7f1 | ||
|
|
384286a453 | ||
|
|
d93b7db69a | ||
|
|
bdbde4bb0f | ||
|
|
e2a68ad903 | ||
|
|
46cfe50108 | ||
|
|
4b423a3087 | ||
|
|
7d0b07e858 | ||
|
|
40220600b0 | ||
|
|
0c7a0e927c | ||
|
|
60e8157b9a | ||
|
|
b8f5e4553b | ||
|
|
de587eb50a | ||
|
|
086bcb7148 | ||
|
|
c22589b5b3 | ||
|
|
e0fbab3c49 | ||
|
|
7cfed38194 | ||
|
|
b8105ca5a6 | ||
|
|
c3ee633897 | ||
|
|
23b41ce9b3 | ||
|
|
2c6156be25 | ||
|
|
eeb3eb3689 | ||
|
|
44a158a714 | ||
|
|
770298b1d5 | ||
|
|
7bd50903a6 | ||
|
|
64295d04ff | ||
|
|
f168ce4499 | ||
|
|
419c349a1f | ||
|
|
26d0f7282f | ||
|
|
93c72d2d92 | ||
|
|
f252871322 | ||
|
|
32c8597305 | ||
|
|
9714512c31 | ||
|
|
f106ff0c21 | ||
|
|
9c85a6bb64 | ||
|
|
dc594f196a | ||
|
|
db4d8a4729 | ||
|
|
0ca1d0ebd0 | ||
|
|
4995902bc0 | ||
|
|
28e55b2843 | ||
|
|
decaace84a | ||
|
|
72f98dc4a3 | ||
|
|
2951c1dac8 | ||
|
|
40a0a16db0 | ||
|
|
520680b037 | ||
|
|
2844bead6e | ||
|
|
d498ec4a45 | ||
|
|
e2d1ca7d0e | ||
|
|
a547327c61 | ||
|
|
e9a7ce9544 | ||
|
|
511be6415c | ||
|
|
4d85053a4d | ||
|
|
4fcf1877c1 | ||
|
|
41fd15fc57 | ||
|
|
8e0845d032 | ||
|
|
c04e533a61 | ||
|
|
54f4997f71 | ||
|
|
3272e995ab | ||
|
|
8edb551571 | ||
|
|
4bf0de4763 | ||
|
|
f6a54436b9 | ||
|
|
7a497cdbd6 | ||
|
|
06336a40d8 | ||
|
|
842c95cfb9 | ||
|
|
aa2f471e87 | ||
|
|
4209d1fc12 | ||
|
|
583f363b6b | ||
|
|
1c633b4624 | ||
|
|
2677511da6 | ||
|
|
387441c045 | ||
|
|
4a2db246e0 |
No files matched your search
@@ -3,6 +3,7 @@
|
||||
*.patch text eol=lf
|
||||
*.java text eol=lf
|
||||
*.gradle text eol=crlf
|
||||
**/MANIFEST.MF text eol=crlf
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.exe binary
|
||||
|
||||
@@ -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.gg/UvedJ9m -->
|
||||
<!-- Discord: https://discord..minecraftforge.net/ -->
|
||||
@@ -11,6 +11,7 @@ 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}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '26.2' ]
|
||||
|
||||
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 }}
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Upload payload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
|
||||
- uses: actions/upload-artifact@v4 # 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@v3 # Payload artifact is consumed in Actionable
|
||||
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
|
||||
with:
|
||||
name: payload
|
||||
path: ${{ github.event_path }}
|
||||
+7
-12
@@ -26,7 +26,6 @@
|
||||
/projects/mcp/
|
||||
/projects/clean/
|
||||
/projects/forge/
|
||||
#/patches/*
|
||||
|
||||
#occupational hazards
|
||||
/projects/**/build/
|
||||
@@ -35,20 +34,16 @@
|
||||
/projects/**/*.launch
|
||||
/repo/
|
||||
src/*/generated/**/.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/
|
||||
|
||||
# FML changelog
|
||||
changelog.txt
|
||||
*.py
|
||||
*.7z
|
||||
/logs/
|
||||
|
||||
/ForgeRoot.ipr
|
||||
/ForgeRoot.iws
|
||||
|
||||
# APT stuff
|
||||
/*/.gitignore
|
||||
/*/.factorypath
|
||||
/*/.apt_generated/
|
||||
/fmlcore/logs/
|
||||
Vendored
-104
@@ -1,104 +0,0 @@
|
||||
<?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>
|
||||
Vendored
-193
@@ -1,193 +0,0 @@
|
||||
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", "21", 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", "8.7", 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.21.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_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."
|
||||
})
|
||||
|
||||
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
@@ -1,65 +0,0 @@
|
||||
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 its 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 stabilized 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.
|
||||
https://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.
|
||||
https://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.
|
||||
|
||||
+9
-7
@@ -4,25 +4,24 @@ import net.minecraftforge.gradleutils.PomUtils
|
||||
plugins {
|
||||
id 'net.minecraftforge.licenser' version '1.0.1'
|
||||
id 'com.github.ben-manes.versions' version '0.46.0'
|
||||
id 'net.minecraftforge.gradleutils' version '[2.3.0,2.4.0)'
|
||||
id 'net.minecraftforge.gradleutils' version '[2.6.4,2.7)'
|
||||
id 'eclipse'
|
||||
id 'de.undercouch.download' version '5.4.0'
|
||||
id 'net.minecraftforge.gradle.patcher' version '[6.0.22,6.2)' apply false
|
||||
id 'net.minecraftforge.gradle.mcp' version '[6.0.22,6.2)' apply false
|
||||
id 'net.minecraftforge.gradle.patcher' version '[6.0.51,6.2)' apply false
|
||||
id 'net.minecraftforge.gradle.mcp' version '[6.0.51,6.2)' apply false
|
||||
id 'net.minecraftforge.gradlejarsigner' version '1.0.4'
|
||||
id 'org.barfuin.gradle.taskinfo' version '2.1.0'
|
||||
//id 'org.barfuin.gradle.taskinfo' version '3.0.1'
|
||||
}
|
||||
|
||||
Util.init() //Init all our extension methods!
|
||||
|
||||
ext {
|
||||
GIT_INFO = gradleutils.gitInfo
|
||||
VERSION = gradleutils.getFilteredMCTagOffsetBranchVersion(true, '[0-9]', MC_VERSION)
|
||||
VERSION = gitversion.getMCTagOffsetBranch(MC_VERSION)
|
||||
FORGE_VERSION = VERSION.substring(MC_VERSION.length() + 1)
|
||||
}
|
||||
|
||||
changelog {
|
||||
from '47.999'
|
||||
from '65.0'
|
||||
}
|
||||
|
||||
tasks.register('setup') {
|
||||
@@ -42,6 +41,9 @@ project(':mcp') {
|
||||
config MC_VERSION + '-' + MCP_VERSION
|
||||
pipeline = 'joined'
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
if (System.env.TEAMCITY_VERSION) {
|
||||
|
||||
@@ -4,10 +4,11 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:9.7'
|
||||
implementation 'org.ow2.asm:asm-tree:9.7'
|
||||
implementation 'net.minecraftforge:srgutils:0.5.10'
|
||||
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.5.0.202303070854-r'
|
||||
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.7.0.202309050840-r'
|
||||
}
|
||||
@@ -10,17 +10,17 @@ import java.util.zip.ZipFile
|
||||
|
||||
abstract class BundleList extends DefaultTask {
|
||||
@InputFiles abstract ConfigurableFileCollection getConfig()
|
||||
@InputFiles abstract ConfigurableFileCollection getConfigExtra()
|
||||
@InputFile abstract RegularFileProperty getServerBundle()
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
|
||||
BundleList() {
|
||||
config.setFrom(project.configurations.installer)
|
||||
configExtra.setFrom(project.configurations.installerextra)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,6 @@ abstract class BundleList extends DefaultTask {
|
||||
entries.put("$info.art.group:$info.art.name", "$dep.file.sha256\t$info.name\t$info.path")
|
||||
}
|
||||
|
||||
resolved = project.configurations.installerextra.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
|
||||
@@ -60,6 +53,10 @@ abstract class BundleList extends DefaultTask {
|
||||
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)
|
||||
}
|
||||
|
||||
+19
-3
@@ -3,6 +3,7 @@ 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
|
||||
|
||||
@@ -12,13 +13,19 @@ abstract class BytecodePredicateFinder extends BytecodeFinder {
|
||||
@Internal
|
||||
abstract Property<Closure<Boolean>> getPredicate()
|
||||
|
||||
private final Set<String> matches = new HashSet<>()
|
||||
private final Map<String, List<String>> matches = new TreeMap<>(Comparator.naturalOrder())
|
||||
|
||||
@Override
|
||||
protected process(ClassNode parent, MethodNode node) {
|
||||
for (final current : node.instructions) {
|
||||
if (predicate.get().call(parent, node, current)) {
|
||||
matches.add(parent.name)
|
||||
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)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -27,6 +34,15 @@ abstract class BytecodePredicateFinder extends BytecodeFinder {
|
||||
@Internal
|
||||
@Override
|
||||
protected Object getData() {
|
||||
return matches.toSorted(Comparator.<String>naturalOrder()) ?: {throw new RuntimeException('Failed to find any targets, please ensure that method names and descriptors are correct.')}()
|
||||
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.') }()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* 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 + ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ abstract class LauncherJson extends DefaultTask {
|
||||
dependsOn(project.tasks.universalJar)
|
||||
input.from(project.tasks.universalJar.archiveFile)
|
||||
input.from(project.configurations.installer)
|
||||
input.from(project.configurations.installerextra)
|
||||
configure {
|
||||
def mc = project.rootProject.ext.MC_VERSION
|
||||
def forge = project.rootProject.ext.FORGE_VERSION
|
||||
@@ -89,7 +88,6 @@ abstract class LauncherJson extends DefaultTask {
|
||||
]
|
||||
])
|
||||
|
||||
json.libraries.addAll(getArtifacts(project.configurations.installerextra).values())
|
||||
json.libraries.addAll(getArtifacts(project.configurations.installer).values())
|
||||
Files.writeString(output.get().asFile.toPath(), new JsonBuilder(json).toPrettyString())
|
||||
}
|
||||
|
||||
@@ -17,17 +17,15 @@ 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, mappings).join('\n')
|
||||
it.text = joinBack(lines, inheritance).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,45 +168,18 @@ abstract class CheckATs extends CheckTask {
|
||||
return lines
|
||||
}
|
||||
|
||||
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance, IMappingFile mappings) {
|
||||
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance) {
|
||||
final data = [] as List<String>
|
||||
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 = remapComment.call(value)
|
||||
def comment = null //value.comment
|
||||
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 = remapComment(entry)
|
||||
final comment = null //entry.comment
|
||||
data.add(line + (comment ? ' ' + comment : ''))
|
||||
}
|
||||
data.add('#endgroup')
|
||||
@@ -289,6 +260,7 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
+135
-209
@@ -47,6 +47,7 @@ jarSigner.autoDetect('forge')
|
||||
applyPatches {
|
||||
level 'WARNING'
|
||||
failOnError = UPDATING != 'true'
|
||||
rejects = rootProject.layout.projectDirectory.dir('rejects').asFile
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -70,7 +71,7 @@ sourceSets {
|
||||
}
|
||||
}
|
||||
|
||||
final String SPEC_VERSION = gradleutils.gitInfo.tag
|
||||
final String SPEC_VERSION = gitversion.info.tag
|
||||
|
||||
// The new versioning sceme is <MCVersion>-<ForgeMC>.<RB>.<CommitsSinceRB>
|
||||
// ForgeMC is a unique identifier for every MC version we have supported.
|
||||
@@ -78,7 +79,6 @@ final String SPEC_VERSION = gradleutils.gitInfo.tag
|
||||
final def MCP_ARTIFACT = project(':mcp').mcp.config.get()
|
||||
|
||||
final List<File> EXTRA_TXTS = [
|
||||
rootProject.file('CREDITS.txt'),
|
||||
rootProject.file('LICENSE.txt'),
|
||||
rootProject.tasks.createChangelog.outputFile
|
||||
]
|
||||
@@ -88,21 +88,17 @@ ext {
|
||||
}
|
||||
final String MAVEN_PATH = ext.MAVEN_PATH
|
||||
|
||||
final String BINPATCH_TOOL = 'net.minecraftforge:binarypatcher:1.2.0:fatjar'
|
||||
final String INSTALLER_TOOLS = 'net.minecraftforge:installertools:1.4.3'
|
||||
final String BINPATCH_TOOL = 'net.minecraftforge:binarypatcher:1.3.0:fatjar'
|
||||
final String INSTALLER_TOOLS = 'net.minecraftforge:installertools:1.4.5'
|
||||
final String FART = 'net.minecraftforge:ForgeAutoRenamingTool:1.0.6'
|
||||
final String S2S_TOOL = 'net.minecraftforge:Srg2Source:8.1.0:fatjar'
|
||||
final String S2S_TOOL = 'net.minecraftforge:Srg2Source:8.2.0:fatjar'
|
||||
|
||||
configurations {
|
||||
// Don't pull all libraries, if we're missing something, add it to the installer list so the installer knows to download it.
|
||||
bootstrap { transitive = false }
|
||||
installer {
|
||||
installer {
|
||||
extendsFrom(bootstrap)
|
||||
transitive = false
|
||||
}
|
||||
installerextra {
|
||||
extendsFrom(bootstrap)
|
||||
transitive = false
|
||||
transitive = false
|
||||
}
|
||||
api.extendsFrom(installer)
|
||||
}
|
||||
@@ -112,28 +108,30 @@ dependencies {
|
||||
// And this is only needed because custom file systems are REQUIRED to be on the boot classloader.
|
||||
// This has ASM/BootStrap/Unsafe all because I haven't gotten around to moving UnionFileSystem out to its own project.
|
||||
bootstrap(libs.jarjar.fs) // JarInJar file system
|
||||
bootstrap(libs.roimfs) // JarInJar File System - FileSystems need to be on boot loader.
|
||||
bootstrap(libs.bundles.jimfs) // In memory file system used for ForgeDev launches
|
||||
bootstrap(libs.securemodules) // Has Union file system in it
|
||||
bootstrap(libs.unsafe) // Needed by securemodules
|
||||
bootstrap(libs.bundles.asm) // Needed by securemodules
|
||||
|
||||
|
||||
implementation(libs.jopt.simple)
|
||||
|
||||
|
||||
installer(libs.bootstrap)
|
||||
installer(libs.bootstrap.api) // Needed by securemodules
|
||||
installer(libs.accesstransformers)
|
||||
installer(libs.eventbus)
|
||||
installer(libs.typetools) // Dep of EventBus
|
||||
installer(libs.jspecify) // Dep of EventBus
|
||||
installer(libs.forgespi)
|
||||
installer(libs.coremods)
|
||||
installer(libs.nashorn) // Dep of Coremods
|
||||
installer(libs.coremods.api)
|
||||
installer(libs.modlauncher)
|
||||
installer(libs.mergetool.api)
|
||||
installer(libs.bundles.night.config)
|
||||
installer(libs.maven.artifact)
|
||||
installer(libs.bundles.terminalconsoleappender)
|
||||
installer(libs.mixin)
|
||||
installer(libs.mixinextras.forge)
|
||||
installer(libs.bundles.jarjar)
|
||||
installer(libs.roimfs)
|
||||
|
||||
installer(project(':fmlcore'))
|
||||
installer(project(':fmlloader'))
|
||||
@@ -141,16 +139,28 @@ dependencies {
|
||||
installer(project(':javafmllanguage'))
|
||||
installer(project(':lowcodelanguage'))
|
||||
installer(project(':mclanguage'))
|
||||
|
||||
installerextra("${MCP_ARTIFACT.group}:${MCP_ARTIFACT.name}:${MCP_ARTIFACT.version}:srg2off")
|
||||
installer(project(':forge-transformers'))
|
||||
|
||||
runtimeOnly(libs.bootstrap)
|
||||
runtimeOnly(libs.bootstrap.dev)
|
||||
|
||||
annotationProcessor(libs.eventbus.validator)
|
||||
}
|
||||
|
||||
tasks.named('extractRangeMap').configure {
|
||||
tool = S2S_TOOL
|
||||
}
|
||||
tasks.named('applyRangeMap').configure {
|
||||
tool = S2S_TOOL
|
||||
}
|
||||
tasks.named('applyRangeMapBase').configure {
|
||||
tool = S2S_TOOL
|
||||
}
|
||||
|
||||
// Disable all tests, we have GameTests not JUnit tests
|
||||
test {
|
||||
exclude '**/*'
|
||||
}
|
||||
|
||||
patcher {
|
||||
excs.from file("$rootDir/src/main/resources/forge.exc")
|
||||
@@ -165,7 +175,7 @@ patcher {
|
||||
|
||||
runs {
|
||||
forge_client {
|
||||
property 'eventbus.checkTypesOnDispatch', 'true'
|
||||
property 'eventbus.api.strictRuntimeChecks', 'true'
|
||||
property 'org.lwjgl.system.SharedLibraryExtractDirectory', 'lwjgl_dll'
|
||||
|
||||
args '--launchTarget', 'forge_dev_client',
|
||||
@@ -194,6 +204,7 @@ patcher {
|
||||
|
||||
forge_server_gametest {
|
||||
args '--launchTarget', 'forge_dev_server_gametest'
|
||||
args '--uniqueWorld' // Unique world is used so that the world regenerates, as well as the world config isn't influenced by other runs
|
||||
}
|
||||
|
||||
forge_server_gametest_test {
|
||||
@@ -205,9 +216,7 @@ patcher {
|
||||
args '--launchTarget', 'forge_dev_data',
|
||||
'--mod', 'forge',
|
||||
'--all',
|
||||
'--validate',
|
||||
'--output', rootProject.file('src/main/generated/'),
|
||||
'--validate',
|
||||
'--existing', sourceSets.main.resources.srcDirs[0],
|
||||
'--assetsDir', downloadAssets.output,
|
||||
'--assetIndex', "{asset_index}"
|
||||
@@ -218,7 +227,30 @@ patcher {
|
||||
args '--launchTarget', 'forge_dev_data',
|
||||
'--mod', '.+',
|
||||
'--all',
|
||||
'--validate',
|
||||
'--output', rootProject.file('src/test/generated/'),
|
||||
'--existing', sourceSets.main.resources.srcDirs[0],
|
||||
'--existing', sourceSets.test.resources.srcDirs[0],
|
||||
'--assetsDir', downloadAssets.output,
|
||||
'--assetIndex', "{asset_index}"
|
||||
}
|
||||
|
||||
forge_client_data {
|
||||
args '--launchTarget', 'forge_dev_client_data',
|
||||
'--mod', 'forge',
|
||||
'--all',
|
||||
'--output', rootProject.file('src/main/generated/'),
|
||||
'--existing', sourceSets.main.resources.srcDirs[0],
|
||||
'--assetsDir', downloadAssets.output,
|
||||
'--assetIndex', "{asset_index}"
|
||||
}
|
||||
|
||||
forge_client_data_test {
|
||||
source sourceSets.test
|
||||
args '--launchTarget', 'forge_dev',
|
||||
'--launchEntry', 'minecraft/net.minecraft.client.data.Main',
|
||||
'--launchData',
|
||||
'--mod', '.+',
|
||||
'--all',
|
||||
'--output', rootProject.file('src/test/generated/'),
|
||||
'--existing', sourceSets.main.resources.srcDirs[0],
|
||||
'--existing', sourceSets.test.resources.srcDirs[0],
|
||||
@@ -237,21 +269,6 @@ afterEvaluate {
|
||||
userdevJar {
|
||||
onlyIf = { t -> true }
|
||||
}
|
||||
def patches = project.file('build/genPatchesForUserdev/output/')
|
||||
patches.mkdirs()
|
||||
def genPatchesForUserdev = tasks.register('genPatchesForUserdev', GeneratePatches){
|
||||
base = project(':mcp').setupMCP.output
|
||||
modified = applyRangeMapBase.output
|
||||
originalPrefix = genPatches.originalPrefix
|
||||
modifiedPrefix = genPatches.modifiedPrefix
|
||||
output = patches
|
||||
autoHeader true
|
||||
lineEnding = '\n'
|
||||
}
|
||||
bakePatches {
|
||||
dependsOn = []
|
||||
input = genPatchesForUserdev.get().output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,11 +283,16 @@ tasks.register('downloadCrowdin', Download) {
|
||||
tasks.userdevConfig.configure {
|
||||
configurations.installer.allDependencies.forEach {
|
||||
def dep = it.toString()
|
||||
if (it instanceof ProjectDependency)
|
||||
if (it instanceof ProjectDependency)
|
||||
dep = "net.minecraftforge:$it.dependencyProject.name:$it.dependencyProject.version"
|
||||
libraries.add(dep)
|
||||
}
|
||||
|
||||
|
||||
// TODO [ForgeDev][UserDev] Split API/Runtime elements in userdev config
|
||||
// so we don't gotta keep making exceptions for MixinExtras
|
||||
addCompileDependency(libs.mixinextras.common)
|
||||
addAnnotationProcessorDependency(libs.mixinextras.common)
|
||||
|
||||
inject = '' // We don't have a userdev sourceset anymore. Empty as a gradle workaround...
|
||||
runs {
|
||||
client {
|
||||
@@ -282,6 +304,13 @@ tasks.userdevConfig.configure {
|
||||
args '--assetsDir', '{assets_root}'
|
||||
}
|
||||
|
||||
clientData {
|
||||
environment 'MCP_MAPPINGS', '{mcp_mappings}'
|
||||
args '--launchTarget', "forge_userdev_client_data"
|
||||
args '--assetIndex', '{asset_index}'
|
||||
args '--assetsDir', '{assets_root}'
|
||||
}
|
||||
|
||||
server {
|
||||
environment 'MCP_MAPPINGS', '{mcp_mappings}'
|
||||
property 'forge.enableGameTest', 'true'
|
||||
@@ -306,7 +335,7 @@ for (def run in patcher.runs + tasks.userdevConfig.runs) {
|
||||
if (run.parents) continue // We already added this to the parent run config
|
||||
//run.property 'bsl.debug', 'true'
|
||||
run.args '--gameDir', '.'
|
||||
run.jvmArgs '-Djava.net.preferIPv6Addresses=system'
|
||||
run.jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders'
|
||||
run.client run.name.contains('client')
|
||||
run.main 'net.minecraftforge.bootstrap.ForgeBootstrap'
|
||||
}
|
||||
@@ -379,14 +408,15 @@ tasks.register('extractInheritance', ExtractInheritance) {
|
||||
}
|
||||
}
|
||||
tasks.register("findFinalizeSpawnTargets", BytecodePredicateFinder) {
|
||||
jar = createJoinedSRG.output
|
||||
output = rootProject.file('src/main/resources/coremods/finalize_spawn_targets.json')
|
||||
dependsOn downloadClientRaw
|
||||
jar = downloadClientRaw.dest
|
||||
output = rootProject.file('forge-transformers/src/main/resources/coremods/finalize_spawn_targets.json')
|
||||
predicate = {
|
||||
parent, node, insn ->
|
||||
return 'net/minecraft/world/level/BaseSpawner' != parent.name // Ignore this class as we special case it.
|
||||
&& insn.getOpcode().equals(Opcodes.INVOKEVIRTUAL)
|
||||
&& insn.name == 'm_6518_'
|
||||
&& insn.desc == '(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/MobSpawnType;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;';
|
||||
&& insn.name == 'finalizeSpawn'
|
||||
&& insn.desc == '(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/EntitySpawnReason;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;';
|
||||
}
|
||||
}
|
||||
tasks.register('validateDeprecations', ValidateDeprecations) {
|
||||
@@ -404,72 +434,39 @@ tasks.register("downloadServerShim", DownloadMavenArtifact) {
|
||||
artifact = libs.bootstrap.shim.get().toString()
|
||||
changing = true
|
||||
}
|
||||
tasks.register("downloadMCPConfig", DownloadMavenArtifact) {
|
||||
artifact = MCP_ARTIFACT
|
||||
}
|
||||
tasks.register('extractMCPConfigMappings', net.minecraftforge.gradle.common.tasks.JarExec) {
|
||||
ext.output = file('build/mcp_mappings.tsrg')
|
||||
tool = INSTALLER_TOOLS + ':fatjar'
|
||||
args = ['--task', 'MCP_DATA', '--key', 'mappings', '--input', downloadMCPConfig.output.get().asFile.absolutePath, '--output', output.absolutePath]
|
||||
outputs.file(output)
|
||||
inputs.file(downloadMCPConfig.output)
|
||||
}
|
||||
tasks.register('downloadClientMappings', net.minecraftforge.gradle.common.tasks.JarExec) {
|
||||
ext.output = file('build/client_mappings.tsrg')
|
||||
tool = INSTALLER_TOOLS + ':fatjar'
|
||||
args = ['--task', 'DOWNLOAD_MOJMAPS', '--sanitize', '--version', MC_VERSION, '--side', 'client', '--output', output.absolutePath]
|
||||
outputs.file(output)
|
||||
}
|
||||
tasks.register('downloadServerMappings', net.minecraftforge.gradle.common.tasks.JarExec) {
|
||||
ext.output = file('build/server_mappings.tsrg')
|
||||
tool = INSTALLER_TOOLS + ':fatjar'
|
||||
args = ['--task', 'DOWNLOAD_MOJMAPS', '--sanitize', '--version', MC_VERSION, '--side', 'server', '--output', output.absolutePath]
|
||||
outputs.file(output)
|
||||
}
|
||||
tasks.register("createJoinedSRG", DownloadMavenArtifact) {
|
||||
artifact = "net.minecraft:joined:${MC_VERSION}-${MCP_VERSION}:srg"
|
||||
}
|
||||
tasks.register("createClientOfficial", RenameJar) {
|
||||
tool = FART + ':all'
|
||||
args = ['--input', '{input}', '--output', '{output}', '--names', '{mappings}', '--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse']
|
||||
mappings = downloadClientMappings.output
|
||||
dependsOn(downloadClientRaw, downloadClientMappings)
|
||||
input = downloadClientRaw.dest
|
||||
output = file("build/createClientOfficial/output.jar")
|
||||
}
|
||||
tasks.register("createServerOfficial", RenameJar) {
|
||||
tool = FART + ':all'
|
||||
args = ['--input', '{input}', '--output', '{output}', '--names', '{mappings}', '--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse']
|
||||
mappings = downloadServerMappings.output
|
||||
dependsOn(downloadServerMappings)
|
||||
input = extractServer.output
|
||||
output = file("build/createServerOfficial/output.jar")
|
||||
artifact = "net.minecraft:joined:${MC_VERSION}-${MCP_VERSION}"
|
||||
}
|
||||
tasks.named('genClientBinPatches').configure {
|
||||
dependsOn(downloadClientRaw)
|
||||
tool = BINPATCH_TOOL
|
||||
cleanJar = createClientOfficial.output
|
||||
cleanJar = downloadClientRaw.dest
|
||||
dirtyJar = jar.archiveFile
|
||||
}
|
||||
tasks.named('genServerBinPatches').configure {
|
||||
dependsOn(extractServer)
|
||||
tool = BINPATCH_TOOL
|
||||
cleanJar = createServerOfficial.output
|
||||
cleanJar = extractServer.output
|
||||
dirtyJar = jar.archiveFile
|
||||
}
|
||||
tasks.named('genJoinedBinPatches').configure {
|
||||
tool = BINPATCH_TOOL
|
||||
cleanJar = createJoinedSRG.output
|
||||
dirtyJar = jar.archiveFile
|
||||
}
|
||||
tasks.register('applyClientBinPatches', ApplyBinPatches) {
|
||||
dependsOn downloadClientRaw
|
||||
tool = BINPATCH_TOOL
|
||||
clean = createClientOfficial.output
|
||||
clean = downloadClientRaw.dest
|
||||
patch = genClientBinPatches.output
|
||||
args.addAll(['--data', '--unpatched'])
|
||||
args.addAll(['--data', '--unpatched', '--store', '--marker', '.forge_patched_minecraft'])
|
||||
}
|
||||
tasks.register('applyServerBinPatches', ApplyBinPatches) {
|
||||
dependsOn extractServer
|
||||
tool = BINPATCH_TOOL
|
||||
clean = createServerOfficial.output
|
||||
clean = extractServer.output
|
||||
patch = genServerBinPatches.output
|
||||
args.addAll(['--data', '--unpatched'])
|
||||
args.addAll(['--data', '--unpatched', '--store'])
|
||||
}
|
||||
tasks.register('applyJoinedBinPatches', ApplyBinPatches) {
|
||||
tool = BINPATCH_TOOL
|
||||
@@ -485,7 +482,7 @@ tasks.register('createServerShimConfig') {
|
||||
doLast {
|
||||
var cfg = new CleanProperties()
|
||||
cfg['Main-Class'] = 'net.minecraftforge.bootstrap.ForgeBootstrap'
|
||||
cfg['Java-Version'] = '21'
|
||||
cfg['Java-Version'] = '25'
|
||||
cfg['Arguments'] = '--launchTarget forge_server'
|
||||
cfg.store(output)
|
||||
}
|
||||
@@ -501,7 +498,7 @@ tasks.register('serverShimJar', Jar) {
|
||||
from {
|
||||
zipTree(downloadServerShim.output).find { it.name == 'MANIFEST.MF' }
|
||||
}
|
||||
|
||||
|
||||
attributes('Class-Path': configurations.bootstrap.resolvedConfiguration.resolvedArtifacts.collect { "libraries/${Util.getMavenInfoFromDep(it).path}" }.join(' '))
|
||||
}
|
||||
archiveClassifier = 'shim'
|
||||
@@ -517,10 +514,9 @@ tasks.register('checkAllAndFix') {
|
||||
}
|
||||
|
||||
CheckTask.registerTask(tasks, 'ATs', CheckATs) {
|
||||
dependsOn extractInheritance, createSrg2Mcp
|
||||
dependsOn extractInheritance
|
||||
ats.from patcher.accessTransformers
|
||||
inheritance = extractInheritance.output
|
||||
mappings = createSrg2Mcp.output
|
||||
}
|
||||
|
||||
CheckTask.registerTask(tasks, 'SAS', CheckSAS) {
|
||||
@@ -594,7 +590,7 @@ tasks.register('checkJarCompatibility', CheckJarCompatibility) {
|
||||
baseLibraries.from(project.tasks.named('createJoinedSRG').flatMap { it.output })
|
||||
|
||||
inputJar = project.tasks.named('reobfJar').flatMap { it.output }
|
||||
|
||||
|
||||
commonLibraries.from(project.configurations.minecraftImplementation)
|
||||
commonLibraries.from(project.configurations.installer)
|
||||
}
|
||||
@@ -607,7 +603,8 @@ tasks.register('launcherJson', LauncherJson).configure {
|
||||
'--launchTarget', 'forge_client'
|
||||
],
|
||||
jvm: [
|
||||
'-Djava.net.preferIPv6Addresses=system'
|
||||
'-Djava.net.preferIPv6Addresses=system',
|
||||
'-XX:+UseCompactObjectHeaders'
|
||||
]
|
||||
]
|
||||
] as LinkedHashMap)
|
||||
@@ -615,23 +612,14 @@ tasks.register('launcherJson', LauncherJson).configure {
|
||||
|
||||
tasks.register('installerJson', InstallerJson) {
|
||||
icon = rootProject.file('icon.ico')
|
||||
|
||||
|
||||
// Json to install into the client's launcher
|
||||
dependsOn(launcherJson)
|
||||
input.from(launcherJson.output)
|
||||
|
||||
// Extract MCP Mappings
|
||||
dependsOn(extractMCPConfigMappings)
|
||||
input.from(extractMCPConfigMappings.output)
|
||||
// Download Official Mappings
|
||||
dependsOn(downloadClientMappings, downloadServerMappings)
|
||||
input.from(downloadClientMappings.output, downloadServerMappings.output)
|
||||
|
||||
// Get 'base' MC jar, Client is straight download, server is extracted from the bundle
|
||||
dependsOn(downloadClientRaw, extractServer)
|
||||
input.from(downloadClientRaw.dest, extractServer.output)
|
||||
// Rename MC Jar
|
||||
dependsOn(createClientOfficial, createServerOfficial)
|
||||
input.from(createClientOfficial.output, createServerOfficial.output)
|
||||
// Apply Binary patches to vanilla jar
|
||||
dependsOn(applyClientBinPatches, applyServerBinPatches)
|
||||
input.from(applyClientBinPatches.output, applyServerBinPatches.output, genClientBinPatches.toolJar)
|
||||
@@ -649,22 +637,6 @@ tasks.register('installerJson', InstallerJson) {
|
||||
minecraft: MC_VERSION,
|
||||
serverJarPath: '{LIBRARY_DIR}/net/minecraft/server/{MINECRAFT_VERSION}/server-{MINECRAFT_VERSION}-bundled.jar',
|
||||
data: [
|
||||
MAPPINGS: [
|
||||
client: "[${MCP_ARTIFACT.group}:${MCP_ARTIFACT.name}:${MCP_ARTIFACT.version}:mappings@tsrg]",
|
||||
server: "[${MCP_ARTIFACT.group}:${MCP_ARTIFACT.name}:${MCP_ARTIFACT.version}:mappings@tsrg]"
|
||||
],
|
||||
MAPPINGS_SHA: [
|
||||
client: "'${extractMCPConfigMappings.output.sha1}'",
|
||||
server: "'${extractMCPConfigMappings.output.sha1}'"
|
||||
],
|
||||
MOJMAPS: [
|
||||
client: "[net.minecraft:client:${MC_VERSION}:mappings@tsrg]",
|
||||
server: "[net.minecraft:server:${MC_VERSION}:mappings@tsrg]"
|
||||
],
|
||||
MOJMAPS_SHA: [
|
||||
client: "'${downloadClientMappings.output.sha1}'",
|
||||
server: "'${downloadServerMappings.output.sha1}'"
|
||||
],
|
||||
MC_UNPACKED: [
|
||||
client: "[net.minecraft:client:${MC_VERSION}]",
|
||||
server: "[net.minecraft:server:${MC_VERSION}:unpacked]"
|
||||
@@ -673,14 +645,6 @@ tasks.register('installerJson', InstallerJson) {
|
||||
client: "'${downloadClientRaw.dest.sha1}'",
|
||||
server: "'${extractServer.output.get().asFile.sha1}'"
|
||||
],
|
||||
MC_OFF: [
|
||||
client: "[net.minecraft:client:${MC_VERSION}:official]",
|
||||
server: "[net.minecraft:server:${MC_VERSION}:official]"
|
||||
],
|
||||
MC_OFF_SHA: [
|
||||
client: "'${createClientOfficial.output.get().asFile.sha1}'",
|
||||
server: "'${createServerOfficial.output.get().asFile.sha1}'"
|
||||
],
|
||||
BINPATCH: [
|
||||
client: '/data/client.lzma',
|
||||
server: '/data/server.lzma'
|
||||
@@ -746,65 +710,28 @@ tasks.register('installerJson', InstallerJson) {
|
||||
outputs: [
|
||||
'{MC_UNPACKED}': '{MC_UNPACKED_SHA}'
|
||||
]
|
||||
], [
|
||||
jar: INSTALLER_TOOLS,
|
||||
classpath: INSTALLER_TOOLS_CLASSPATH,
|
||||
args: [
|
||||
'--task', 'MCP_DATA',
|
||||
'--input', "[${MCP_ARTIFACT.descriptor}]",
|
||||
'--output', '{MAPPINGS}',
|
||||
'--key', 'mappings'
|
||||
],
|
||||
outputs: [
|
||||
'{MAPPINGS}': '{MAPPINGS_SHA}'
|
||||
]
|
||||
], [
|
||||
jar: INSTALLER_TOOLS,
|
||||
classpath: INSTALLER_TOOLS_CLASSPATH,
|
||||
args: [
|
||||
'--task', 'DOWNLOAD_MOJMAPS',
|
||||
'--sanitize',
|
||||
'--version', MC_VERSION,
|
||||
'--side', '{SIDE}',
|
||||
'--output', '{MOJMAPS}'
|
||||
],
|
||||
outputs: [
|
||||
'{MOJMAPS}': '{MOJMAPS_SHA}'
|
||||
]
|
||||
], [
|
||||
sides: ['server'],
|
||||
jar: FART,
|
||||
classpath: getClasspath(project, libs, FART),
|
||||
args: [
|
||||
'--input', '{MC_UNPACKED}',
|
||||
'--output', '{MC_OFF}',
|
||||
'--names', '{MOJMAPS}',
|
||||
'--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse'
|
||||
],
|
||||
outputs: [
|
||||
'{MC_OFF}': '{MC_OFF_SHA}'
|
||||
]
|
||||
], [
|
||||
sides: ['client'],
|
||||
jar: FART,
|
||||
classpath: getClasspath(project, libs, FART),
|
||||
args: [
|
||||
'--input', '{MINECRAFT_JAR}',
|
||||
'--output', '{MC_OFF}',
|
||||
'--names', '{MOJMAPS}',
|
||||
'--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse'
|
||||
],
|
||||
outputs: [
|
||||
'{MC_OFF}': '{MC_OFF_SHA}'
|
||||
]
|
||||
], [
|
||||
jar: BINPATCH_TOOL.rsplit(':', 1)[0], // remove :fatjar
|
||||
classpath: getClasspath(project, libs, BINPATCH_TOOL.rsplit(':', 1)[0]),
|
||||
args: [
|
||||
'--clean', '{MC_OFF}',
|
||||
'--clean', '{MC_UNPACKED}',
|
||||
'--output', '{PATCHED}',
|
||||
'--apply', '{BINPATCH}',
|
||||
'--data', '--unpatched'
|
||||
'--data', '--unpatched', '--store'
|
||||
],
|
||||
outputs: [
|
||||
'{PATCHED}': '{PATCHED_SHA}'
|
||||
]
|
||||
], [
|
||||
sides: ['client'],
|
||||
jar: BINPATCH_TOOL.rsplit(':', 1)[0], // remove :fatjar
|
||||
classpath: getClasspath(project, libs, BINPATCH_TOOL.rsplit(':', 1)[0]),
|
||||
args: [
|
||||
'--clean', '{MINECRAFT_JAR}',
|
||||
'--output', '{PATCHED}',
|
||||
'--apply', '{BINPATCH}',
|
||||
'--data', '--unpatched', '--store', '--marker', '.forge_patched_minecraft'
|
||||
],
|
||||
outputs: [
|
||||
'{PATCHED}': '{PATCHED_SHA}'
|
||||
@@ -829,12 +756,7 @@ tasks.register('officialClassesOnly', Zip).configure {
|
||||
tasks.named('filterJarNew').configure {
|
||||
dependsOn('officialClassesOnly')
|
||||
input = officialClassesOnly.archiveFile
|
||||
}
|
||||
|
||||
tasks.register('filterJarNewSRG', FilterNewJar).configure {
|
||||
input = reobfJar.output
|
||||
srg = filterJarNew.srg
|
||||
blacklist = filterJarNew.blacklist
|
||||
filterInners = true
|
||||
}
|
||||
|
||||
tasks.named('universalJar').configure {
|
||||
@@ -848,6 +770,9 @@ tasks.named('universalJar').configure {
|
||||
exclude '.cache'
|
||||
|
||||
manifest {
|
||||
attributes([
|
||||
'Automatic-Module-Name': 'net.minecraftforge.forge'
|
||||
] as LinkedHashMap)
|
||||
attributes([
|
||||
'Specification-Title': 'Forge',
|
||||
'Specification-Vendor': 'Forge Development LLC',
|
||||
@@ -868,18 +793,8 @@ tasks.named('universalJar').configure {
|
||||
jarSigner.sign(it)
|
||||
}
|
||||
|
||||
tasks.register('universalJarSrg', Jar).configure {
|
||||
from zipTree(filterJarNewSRG.output)
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from zipTree(universalJar.archiveFile)
|
||||
destinationDirectory = file('build/libs')
|
||||
archiveClassifier = 'universal-srg'
|
||||
manifest.from(universalJar.manifest)
|
||||
jarSigner.sign(it)
|
||||
}
|
||||
|
||||
tasks.named('userdevConfig').configure {
|
||||
universal = "$project.group:$project.name:$project.version:universal-srg@jar"
|
||||
universal = "$project.group:$project.name:$project.version:universal@jar"
|
||||
}
|
||||
|
||||
tasks.register('installerJar', InstallerJar) {
|
||||
@@ -921,23 +836,31 @@ tasks.register('installerJar', InstallerJar) {
|
||||
jarSigner.sign(it)
|
||||
}
|
||||
|
||||
final mdkGradleWrapper = tasks.register('mdkGradleWrapper', Wrapper) {
|
||||
gradleVersion = '9.3.1'
|
||||
}
|
||||
|
||||
tasks.register('mdkZip', Zip) {
|
||||
dependsOn mdkGradleWrapper
|
||||
|
||||
archiveBaseName = project.name
|
||||
archiveClassifier = 'mdk'
|
||||
archiveVersion = project.version
|
||||
destinationDirectory = file('build/libs')
|
||||
|
||||
from rootProject.file('gradlew')
|
||||
from rootProject.file('gradlew.bat')
|
||||
from mdkGradleWrapper.map(Wrapper.&getScriptFile)
|
||||
from mdkGradleWrapper.map(Wrapper.&getBatchScript)
|
||||
from(EXTRA_TXTS)
|
||||
from(rootProject.file('gradle/')){
|
||||
into('gradle/')
|
||||
into('gradle/wrapper/') {
|
||||
from mdkGradleWrapper.map(Wrapper.&getJarFile)
|
||||
from mdkGradleWrapper.map(Wrapper.&getPropertiesFile)
|
||||
}
|
||||
from(rootProject.file('mdk/')){
|
||||
rootProject.file('mdk/gitignore.txt').eachLine{
|
||||
rootProject.file('mdk/gitignore.txt').eachLine {
|
||||
if (!it.trim().isEmpty() && !it.trim().startsWith('#'))
|
||||
exclude it
|
||||
}
|
||||
|
||||
filter(ReplaceTokens, tokens: [
|
||||
FORGE_VERSION: FORGE_VERSION,
|
||||
FORGE_GROUP: project.group,
|
||||
@@ -946,11 +869,15 @@ tasks.register('mdkZip', Zip) {
|
||||
MAPPING_CHANNEL: MAPPING_CHANNEL,
|
||||
MAPPING_VERSION: MAPPING_VERSION,
|
||||
FORGE_SPEC_VERSION: SPEC_VERSION.split("\\.")[0],
|
||||
MC_NEXT_VERSION: MC_NEXT_VERSION
|
||||
MC_NEXT_VERSION: MC_NEXT_VERSION,
|
||||
EVENTBUS_VERSION: libs.versions.eventbus.get()
|
||||
])
|
||||
rename 'gitignore\\.txt', '.gitignore'
|
||||
rename 'gitattributes\\.txt', '.gitattributes'
|
||||
}
|
||||
from(rootProject.file('src/test/java/com/example/examplemod/')) {
|
||||
into('src/main/java/com/example/examplemod/')
|
||||
}
|
||||
from(rootProject.file('src/test/generated/mdk_datagen/')) {
|
||||
into('src/main/resources/')
|
||||
exclude '**/.cache/'
|
||||
@@ -974,7 +901,7 @@ license {
|
||||
}
|
||||
|
||||
tasks.register('genAllData') {
|
||||
dependsOn 'forge_data', 'forge_test_data'
|
||||
dependsOn 'forge_data', 'forge_data_test', 'forge_client_data_test'
|
||||
}
|
||||
|
||||
if (project.hasProperty('UPDATE_MAPPINGS')) {
|
||||
@@ -990,7 +917,7 @@ if (project.hasProperty('UPDATE_MAPPINGS')) {
|
||||
|
||||
tasks.named('javadoc', Javadoc).configure {
|
||||
description 'Generates the combined javadocs for the FML projects and the main Forge project'
|
||||
var includedProjects = [ ':fmlcore', ':fmlloader', ':javafmllanguage', ':mclanguage' ]
|
||||
var includedProjects = [ ':fmlcore', ':fmlloader', ':javafmllanguage', ':mclanguage', ':forge-transformers' ]
|
||||
source includedProjects.collect { project(it).sourceSets.main.allJava }
|
||||
classpath = classpath + files(includedProjects.collect { project(it).sourceSets.main.compileClasspath })
|
||||
|
||||
@@ -1068,7 +995,6 @@ tasks.named('javadoc', Javadoc).configure {
|
||||
publishing {
|
||||
publications.register('mavenJava', MavenPublication).configure {
|
||||
artifact universalJar
|
||||
artifact universalJarSrg
|
||||
artifact installerJar
|
||||
artifact mdkZip
|
||||
artifact userdevJar
|
||||
@@ -1084,7 +1010,7 @@ publishing {
|
||||
license gradleutils.pom.Licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
|
||||
@@ -38,12 +38,17 @@ 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
|
||||
|
||||
+16
-8
@@ -2,8 +2,8 @@
|
||||
|
||||
MinecraftForge
|
||||
=============
|
||||
[][Download]
|
||||
[][Download]
|
||||
[][Download] [][Discord] [][Patreon]
|
||||
|
||||
Forge is a free, open-source modding API all of your favourite mods use!
|
||||
@@ -34,12 +34,13 @@ For support and questions, visit [the Support Forum][Support-Forum] or [the Forg
|
||||
|
||||
We require the version of Java that Minecraft requires, which is currently:
|
||||
|
||||
| Minecraft | Java |
|
||||
|---------------|-------------------|
|
||||
| 1.20.6+ | [21][Adoptium-21] |
|
||||
| 1.18 - 1.20.4 | [17][Adoptium-17] |
|
||||
| 1.17.1 | [16][Adoptium-16] |
|
||||
| 1.16.5- | [8][Adoptium-8] |
|
||||
| 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].**
|
||||
|
||||
@@ -70,11 +71,17 @@ 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
|
||||
[](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
|
||||
@@ -93,3 +100,4 @@ Please ensure you have a valid email address associated with your GitHub account
|
||||
[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/
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/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
|
||||
@@ -10,9 +10,10 @@ plugins {
|
||||
apply from: rootProject.file('build_shared.gradle')
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.nulls)
|
||||
compileOnly(libs.jetbrains.annotations)
|
||||
|
||||
api(libs.eventbus)
|
||||
annotationProcessor(libs.eventbus.validator)
|
||||
|
||||
implementation(project(':fmlloader'))
|
||||
implementation(libs.commons.io)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
|
||||
@@ -29,7 +29,7 @@ public class Bindings {
|
||||
/**
|
||||
* @return A supplier of net.minecraftforge.common.MinecraftForge#EVENT_BUS
|
||||
*/
|
||||
public static Supplier<IEventBus> getForgeBus() {
|
||||
public static Supplier<BusGroup> getForgeBus() {
|
||||
return PROVIDER.getForgeBusSupplier();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,14 @@ import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class CrashReportCallables
|
||||
{
|
||||
public class CrashReportCallables {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final List<ISystemReportExtender> crashCallables = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* Register a custom {@link ISystemReportExtender}
|
||||
*/
|
||||
public static void registerCrashCallable(ISystemReportExtender callable)
|
||||
{
|
||||
public static void registerCrashCallable(ISystemReportExtender callable) {
|
||||
crashCallables.add(callable);
|
||||
}
|
||||
|
||||
@@ -33,19 +31,15 @@ public class CrashReportCallables
|
||||
* @param headerName The name of the system report entry
|
||||
* @param reportGenerator The report generator to be called when a crash report is built
|
||||
*/
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator)
|
||||
{
|
||||
registerCrashCallable(new ISystemReportExtender()
|
||||
{
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator) {
|
||||
registerCrashCallable(new ISystemReportExtender() {
|
||||
@Override
|
||||
public String getLabel()
|
||||
{
|
||||
public String getLabel() {
|
||||
return headerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get()
|
||||
{
|
||||
public String get() {
|
||||
return reportGenerator.get();
|
||||
}
|
||||
});
|
||||
@@ -58,31 +52,23 @@ public class CrashReportCallables
|
||||
* @param reportGenerator The report generator to be called when a crash report is built
|
||||
* @param active The supplier of the flag to be checked when a crash report is built
|
||||
*/
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator, BooleanSupplier active)
|
||||
{
|
||||
registerCrashCallable(new ISystemReportExtender()
|
||||
{
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator, BooleanSupplier active) {
|
||||
registerCrashCallable(new ISystemReportExtender() {
|
||||
@Override
|
||||
public String getLabel()
|
||||
{
|
||||
public String getLabel() {
|
||||
return headerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get()
|
||||
{
|
||||
public String get() {
|
||||
return reportGenerator.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive()
|
||||
{
|
||||
try
|
||||
{
|
||||
public boolean isActive() {
|
||||
try {
|
||||
return active.getAsBoolean();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
} catch (Throwable t) {
|
||||
LOGGER.warn("CrashCallable '{}' threw an exception while checking the active flag, disabling", headerName, t);
|
||||
return false;
|
||||
}
|
||||
@@ -90,8 +76,7 @@ public class CrashReportCallables
|
||||
});
|
||||
}
|
||||
|
||||
public static List<ISystemReportExtender> allCrashCallables()
|
||||
{
|
||||
public static List<ISystemReportExtender> allCrashCallables() {
|
||||
return List.copyOf(crashCallables);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,9 @@ public class DeferredWorkQueue
|
||||
LOGGER.debug(LOADING, "Dispatching synchronous work for work queue {}: {} jobs", modLoadingStage, tasks.size());
|
||||
RuntimeException aggregate = new RuntimeException();
|
||||
Stopwatch timer = Stopwatch.createStarted();
|
||||
tasks.forEach(t -> makeRunnable(t, Runnable::run, aggregate));
|
||||
for (TaskInfo t : tasks) {
|
||||
makeRunnable(t, Runnable::run, aggregate);
|
||||
}
|
||||
timer.stop();
|
||||
if (aggregate.getSuppressed().length > 0) {
|
||||
LOGGER.fatal(
|
||||
@@ -114,7 +116,7 @@ public class DeferredWorkQueue
|
||||
return future;
|
||||
}
|
||||
|
||||
private static class TaskInfo {
|
||||
private static final class TaskInfo {
|
||||
private final ModContainer owner;
|
||||
private Runnable task;
|
||||
private CompletableFuture<?> future;
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* 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.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface IBindingsProvider {
|
||||
Supplier<IEventBus> getForgeBusSupplier();
|
||||
Supplier<BusGroup> getForgeBusSupplier();
|
||||
Supplier<I18NParser> getMessageParser();
|
||||
Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration();
|
||||
}
|
||||
@@ -5,16 +5,15 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
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.ToIntFunction;
|
||||
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,
|
||||
@@ -22,6 +21,7 @@ import java.util.function.ToIntFunction;
|
||||
*
|
||||
* @see IModStateProvider
|
||||
*/
|
||||
@NullMarked
|
||||
public interface IModLoadingState {
|
||||
/**
|
||||
* {@return the name of this state}
|
||||
@@ -44,44 +44,46 @@ public interface IModLoadingState {
|
||||
/**
|
||||
* {@return a function returning a human-friendly message for this state}
|
||||
*/
|
||||
Function<ModList, String> message();
|
||||
Supplier<String> message();
|
||||
|
||||
/**
|
||||
* @return a function that computes the size of this transition based on the size of the modlist.
|
||||
* Used to compute progress.
|
||||
*/
|
||||
ToIntFunction<ModList> size();
|
||||
default IntSupplier size() {
|
||||
return ModList::size;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return an optional runnable, which runs before starting the transition from this state to the next}
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
Optional<Consumer<ModList>> inlineRunnable();
|
||||
@Nullable Runnable inlineRunnable();
|
||||
|
||||
/**
|
||||
* Builds the transition task for this state with a blank pre-sync and post-sync task.
|
||||
*
|
||||
* @param <T> a type of event fired on the mod-specific event bus
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @param progressBar a progress meter for tracking progress
|
||||
* @return a transition task for this state
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
default <T extends Event & IModBusEvent>
|
||||
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar) {
|
||||
default @Nullable CompletableFuture<Void> buildTransition(
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar
|
||||
) {
|
||||
return buildTransition(syncExecutor, parallelExecutor, progressBar,
|
||||
e -> CompletableFuture.runAsync(() -> {}, e),
|
||||
e -> CompletableFuture.runAsync(() -> {}, e));
|
||||
_ -> CompletableFuture.completedFuture(null),
|
||||
_ -> CompletableFuture.completedFuture(null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <T> a type of event fired on the mod-specific event bus
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @param progressBar a progress meter for tracking progress
|
||||
@@ -89,8 +91,7 @@ public interface IModLoadingState {
|
||||
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
|
||||
* @return a transition task for this state
|
||||
*/
|
||||
<T extends Event & IModBusEvent>
|
||||
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
|
||||
@Nullable CompletableFuture<Void> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
|
||||
@@ -5,116 +5,47 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
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;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IModStateTransition {
|
||||
static IModStateTransition buildNoopTransition() {
|
||||
return new NoopTransition();
|
||||
return ModStateTransitionHelper.NOOP;
|
||||
}
|
||||
|
||||
default <T extends Event & IModBusEvent>
|
||||
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) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
|
||||
this.eventFunctionStream().get()
|
||||
.map(f -> (EventGenerator<T>) f)
|
||||
.reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, head, ModLoadingStage::currentState, tail))
|
||||
.ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, 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
|
||||
.thenApplyAsync(n -> {
|
||||
progressBar.label(progressBar.name() + ": dispatching "+name);
|
||||
return null;
|
||||
}, parallelExecutor)
|
||||
.thenComposeAsync(n -> eventDispatchCF, parallelExecutor)
|
||||
.thenApply(r -> {
|
||||
postSyncTask.apply(syncExecutor);
|
||||
return null;
|
||||
});
|
||||
return this.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
|
||||
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 BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextModLoadingStage() {
|
||||
return ModLoadingStage::nextState;
|
||||
}
|
||||
|
||||
private <T extends Event & IModBusEvent>
|
||||
EventGenerator<T> addCompletableFutureTaskForModDispatch(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final List<CompletableFuture<Void>> completableFutures,
|
||||
final ProgressMeter progressBar,
|
||||
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, progressBar, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor)));
|
||||
|
||||
var postDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) postDispatchHook();
|
||||
completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator));
|
||||
|
||||
return nextGenerator;
|
||||
@Nullable
|
||||
default <T extends IModBusEvent> EventGenerator<T> eventFunction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 Event & IModBusEvent> extends Function<ModContainer, T> {
|
||||
static <FN extends Event & IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
|
||||
interface EventGenerator<T extends IModBusEvent> extends Function<ModContainer, T> {
|
||||
static <FN extends 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);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ package net.minecraftforge.fml;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface ISystemReportExtender extends Supplier<String>
|
||||
{
|
||||
public interface ISystemReportExtender extends Supplier<String> {
|
||||
String getLabel();
|
||||
|
||||
default boolean isActive()
|
||||
{
|
||||
default boolean isActive() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,63 +5,20 @@
|
||||
|
||||
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 class InterModComms
|
||||
{
|
||||
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {
|
||||
public final class InterModComms {
|
||||
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {}
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Send IMC to remote. Sender will default to the active modcontainer, or minecraft if not.
|
||||
@@ -72,8 +29,9 @@ public 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.get().isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(ModLoadingContext.get().getActiveContainer().getModId(), modId, method, thing));
|
||||
if (!ModList.isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
|
||||
.add(new IMCMessage(ModLoadingContext.get().getContainer().getModId(), modId, method, thing));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -87,8 +45,9 @@ public 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.get().isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(senderModId, modId, method, thing));
|
||||
if (!ModList.isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
|
||||
.add(new IMCMessage(senderModId, modId, method, thing));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,19 +71,17 @@ public class InterModComms
|
||||
* @return All messages
|
||||
*/
|
||||
public static Stream<IMCMessage> getMessages(final String modId) {
|
||||
return getMessages(modId, s->Boolean.TRUE);
|
||||
return getMessages(modId, _ -> true);
|
||||
}
|
||||
|
||||
private static class QueueFilteringSpliterator implements Spliterator<IMCMessage>
|
||||
{
|
||||
private final ConcurrentLinkedQueue<IMCMessage> queue;
|
||||
private final Predicate<String> methodFilter;
|
||||
private final Iterator<IMCMessage> iterator;
|
||||
private record QueueFilteringSpliterator(
|
||||
ConcurrentLinkedQueue<IMCMessage> queue,
|
||||
Predicate<String> methodFilter,
|
||||
Iterator<IMCMessage> iterator
|
||||
) implements Spliterator<IMCMessage> {
|
||||
|
||||
public QueueFilteringSpliterator(final ConcurrentLinkedQueue<IMCMessage> queue, final Predicate<String> methodFilter) {
|
||||
this.queue = queue;
|
||||
this.iterator = queue.iterator();
|
||||
this.methodFilter = methodFilter;
|
||||
this(queue, methodFilter, queue.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,13 +95,10 @@ public 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();
|
||||
@@ -159,6 +113,5 @@ public class InterModComms
|
||||
public Spliterator<IMCMessage> trySplit() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,20 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@@ -33,26 +29,19 @@ 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 HashMap<>();
|
||||
protected final Map<ModLoadingStage, Runnable> activityMap = new EnumMap<>(ModLoadingStage.class);
|
||||
protected final Map<Class<? extends IExtensionPoint<?>>, Supplier<?>> extensionPoints = new IdentityHashMap<>();
|
||||
protected final EnumMap<ModConfig.Type, ModConfig> configs = new EnumMap<>(ModConfig.Type.class);
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
|
||||
final Set<ModContainer> dependencies = new HashSet<>();
|
||||
|
||||
public ModContainer(IModInfo info)
|
||||
{
|
||||
public ModContainer(IModInfo info) {
|
||||
this.modId = info.getModId();
|
||||
// TODO: Currently not reading namespace from configuration..
|
||||
this.namespace = this.modId;
|
||||
@@ -64,7 +53,7 @@ public abstract class ModContainer
|
||||
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()));
|
||||
(incoming, _) -> 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;
|
||||
case "IGNORE_ALL_VERSION" -> // Ignores all information and provides no information
|
||||
@@ -82,58 +71,35 @@ 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 static <T extends Event & IModBusEvent> CompletableFuture<Void> buildTransitionHandler(
|
||||
final ModContainer target,
|
||||
final IModStateTransition.EventGenerator<T> eventGenerator,
|
||||
final ProgressMeter progressBar,
|
||||
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);
|
||||
progressBar.increment();
|
||||
ModLoadingContext.get().setActiveContainer(null);
|
||||
});
|
||||
}
|
||||
|
||||
public IModInfo getModInfo()
|
||||
{
|
||||
public IModInfo getModInfo() {
|
||||
return modInfo;
|
||||
}
|
||||
|
||||
@@ -142,8 +108,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -167,9 +132,7 @@ public abstract class ModContainer
|
||||
configs.put(modConfig.getType(), modConfig);
|
||||
}
|
||||
|
||||
public void dispatchConfigEvent(IConfigEvent event) {
|
||||
configHandler.ifPresent(configHandler->configHandler.accept(event));
|
||||
}
|
||||
public void dispatchConfigEvent(IConfigEvent event) {}
|
||||
|
||||
/**
|
||||
* Does this mod match the supplied mod?
|
||||
@@ -184,9 +147,17 @@ 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 Event & IModBusEvent> void acceptEvent(T e) {}
|
||||
protected <T extends IModBusEvent> void acceptEvent(T e) {}
|
||||
}
|
||||
@@ -5,37 +5,22 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
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;
|
||||
@@ -45,195 +30,131 @@ 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 class ModList
|
||||
{
|
||||
//private static final 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;
|
||||
public final class ModList {
|
||||
private ModList() {}
|
||||
|
||||
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 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 String getModContainerState(String modId) {
|
||||
private static String getModContainerState(String modId) {
|
||||
return getModContainerById(modId).map(ModContainer::getCurrentState).map(Object::toString).orElse("NONE");
|
||||
}
|
||||
|
||||
private String fileToLine(IModFile mf) {
|
||||
private static String fileToLine(IModFile mf) {
|
||||
var mainMod = mf.getModInfos().getFirst();
|
||||
return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|%-10.10s|Manifest: %s", mf.getFileName(),
|
||||
mf.getModInfos().get(0).getDisplayName(),
|
||||
mf.getModInfos().get(0).getModId(),
|
||||
mf.getModInfos().get(0).getVersion(),
|
||||
getModContainerState(mf.getModInfos().get(0).getModId()),
|
||||
mainMod.getDisplayName(),
|
||||
mainMod.getModId(),
|
||||
mainMod.getVersion(),
|
||||
getModContainerState(mainMod.getModId()),
|
||||
((ModFileInfo)mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE"));
|
||||
}
|
||||
private String crashReport() {
|
||||
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
|
||||
private static String crashReport() {
|
||||
return "\n" + applyForEachModFile(ModList::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList)
|
||||
{
|
||||
INSTANCE = new ModList(modFiles, sortedList);
|
||||
return INSTANCE;
|
||||
public static void init() {}
|
||||
|
||||
public static List<IModFileInfo> getModFiles() {
|
||||
return MOD_FILES;
|
||||
}
|
||||
|
||||
public static ModList get() {
|
||||
return INSTANCE;
|
||||
public static IModFileInfo getModFileById(String modid) {
|
||||
return FILE_BY_ID.get(modid);
|
||||
}
|
||||
|
||||
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 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()));
|
||||
}
|
||||
|
||||
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 ProgressMeter progressBar,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChange) {
|
||||
return executor -> gather(
|
||||
this.mods.stream()
|
||||
.map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, progressBar, stateChange, executor))
|
||||
.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).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()));
|
||||
static void clearLoadedMods() {
|
||||
mods = List.of();
|
||||
indexedMods = Map.of();
|
||||
sortedContainers = List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getModObjectById(String modId)
|
||||
{
|
||||
public static <T> Optional<T> getModObjectById(String modId) {
|
||||
return getModContainerById(modId).map(ModContainer::getMod).map(o -> (T) o);
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerById(String modId)
|
||||
{
|
||||
return Optional.ofNullable(this.indexedMods.get(modId));
|
||||
public static Optional<? extends ModContainer> getModContainerById(String modId) {
|
||||
return Optional.ofNullable(indexedMods.get(modId));
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerByObject(Object obj)
|
||||
{
|
||||
public static Optional<? extends ModContainer> getModContainerByObject(Object obj) {
|
||||
return mods.stream().filter(mc -> mc.getMod() == obj).findFirst();
|
||||
}
|
||||
|
||||
public List<IModInfo> getMods()
|
||||
{
|
||||
return this.sortedList;
|
||||
public static List<IModInfo> getMods() {
|
||||
return SORTED_LIST;
|
||||
}
|
||||
|
||||
public boolean isLoaded(String modTarget)
|
||||
{
|
||||
return this.indexedMods.containsKey(modTarget);
|
||||
public static boolean isLoaded(String modTarget) {
|
||||
return indexedMods.containsKey(modTarget);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
public static int size() {
|
||||
return mods.size();
|
||||
}
|
||||
|
||||
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());
|
||||
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();
|
||||
}
|
||||
return modFileScanData;
|
||||
|
||||
return LazyInit.MOD_FILE_SCAN_DATA;
|
||||
}
|
||||
|
||||
public void forEachModFile(Consumer<IModFile> fileConsumer)
|
||||
{
|
||||
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
|
||||
public static void forEachModFile(Consumer<IModFile> fileConsumer) {
|
||||
MOD_FILES.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
|
||||
return modFiles.stream().map(IModFileInfo::getFile).map(function);
|
||||
public static <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
|
||||
return MOD_FILES.stream().map(IModFileInfo::getFile).map(function);
|
||||
}
|
||||
|
||||
public void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
|
||||
public static void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
|
||||
indexedMods.forEach(modContainerConsumer);
|
||||
}
|
||||
|
||||
public void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
|
||||
this.sortedContainers.forEach(containerConsumer);
|
||||
public static void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
|
||||
sortedContainers.forEach(containerConsumer);
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
|
||||
public static List<ModContainer> getLoadedMods() {
|
||||
return sortedContainers;
|
||||
}
|
||||
|
||||
public static <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,17 +5,11 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Streams;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import net.minecraftforge.fml.loading.*;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
@@ -33,8 +27,6 @@ import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static net.minecraftforge.fml.Logging.CORE;
|
||||
import static net.minecraftforge.fml.Logging.LOADING;
|
||||
|
||||
@@ -74,157 +66,198 @@ import static net.minecraftforge.fml.Logging.LOADING;
|
||||
* and completes the mod loading sequence.</dd>
|
||||
* </dl>
|
||||
*/
|
||||
public class ModLoader
|
||||
{
|
||||
public final class ModLoader {
|
||||
private ModLoader() {}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static ModLoader INSTANCE;
|
||||
private final LoadingModList loadingModList;
|
||||
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 final List<ModLoadingException> loadingExceptions;
|
||||
private final List<ModLoadingWarning> loadingWarnings;
|
||||
private final ModStateManager stateManager;
|
||||
private boolean loadingStateValid;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<Consumer<String>> statusConsumer = StartupNotificationManager.modLoaderConsumer();
|
||||
private final Set<IModLoadingState> completedStates = new HashSet<>();
|
||||
private ModList modList;
|
||||
|
||||
private ModLoader()
|
||||
{
|
||||
INSTANCE = this;
|
||||
this.loadingModList = FMLLoader.getLoadingModList();
|
||||
this.loadingExceptions = this.loadingModList.getErrors().stream()
|
||||
.flatMap(ModLoadingException::fromEarlyException)
|
||||
.collect(Collectors.toList());
|
||||
this.loadingWarnings = this.loadingModList.getBrokenFiles().stream()
|
||||
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
|
||||
.collect(Collectors.toList());
|
||||
this.loadingModList.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 computeLanguageList() {
|
||||
return "\n" + LanguageLoadingProvider
|
||||
.applyForEach(lp -> lp.name() + '@' + lp.getClass().getPackage().getImplementationVersion())
|
||||
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
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() {
|
||||
private static 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 void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
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()));
|
||||
loadingStateValid = true;
|
||||
FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask);
|
||||
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));
|
||||
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
if (!LOADING_EXCEPTIONS.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Error during pre-loading phase", LOADING_EXCEPTIONS.getFirst());
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
ModList.clearLoadedMods();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
ModList.clearLoadedMods();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(failedBounds.stream()
|
||||
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
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));
|
||||
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
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();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(LOADING_EXCEPTIONS);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
modList.setLoadedMods(modContainers);
|
||||
this.modList = modList;
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.GATHER);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
|
||||
stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress));
|
||||
progress.complete();
|
||||
}
|
||||
|
||||
public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.LOAD);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
|
||||
stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress));
|
||||
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 finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.COMPLETE);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
|
||||
stateList.forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress));
|
||||
statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())));
|
||||
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();
|
||||
}
|
||||
|
||||
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) {
|
||||
private static void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) {
|
||||
if (!isLoadingStateValid()) {
|
||||
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
|
||||
return;
|
||||
}
|
||||
progressBar.label(progressBar.name()+ " working");
|
||||
progressBar.label(progressBar.name() + " working");
|
||||
syncExecutor.drive(ticker);
|
||||
state.inlineRunnable().ifPresent(a->this.handleInlineTransition(a, state, syncExecutor, ticker));
|
||||
state.buildTransition(syncExecutor, parallelExecutor, progressBar).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
|
||||
completedStates.add(state);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void handleInlineTransition(final Consumer<ModList> transition, final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker) {
|
||||
var pb = StartupMessageManager.addProgressBar("State transition " +state.name()+" running", 0);
|
||||
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.accept(this.modList);
|
||||
transition.run();
|
||||
syncExecutor.drive(ticker);
|
||||
pb.complete();
|
||||
syncExecutor.drive(ticker);
|
||||
}
|
||||
private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<Void> transition) {
|
||||
|
||||
private static 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) {
|
||||
@@ -234,36 +267,38 @@ public class ModLoader
|
||||
.anyMatch(obj -> !(obj instanceof ModLoadingException));
|
||||
if (hasNotModLoadingEx) {
|
||||
LOGGER.fatal("Encountered non-modloading exceptions!", e);
|
||||
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
|
||||
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)
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size());
|
||||
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
throw new LoadingFailedException(modLoadingExceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ModContainer> buildMods(final IModFile modFile)
|
||||
{
|
||||
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
|
||||
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);
|
||||
|
||||
LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader());
|
||||
final List<ModContainer> containers = modFile.getScanResult().getTargets()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> buildModContainerFromTOML(modFile, modInfoMap, e))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
@@ -276,23 +311,26 @@ public class ModLoader
|
||||
missingMods.removeAll(modIds);
|
||||
LOGGER.fatal(LOADING, "The following mods are missing, but have classes in the jar: {}", missingMods);
|
||||
|
||||
loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath()));
|
||||
LOADING_EXCEPTIONS.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();
|
||||
}
|
||||
|
||||
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
|
||||
private static ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap,
|
||||
final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader
|
||||
) {
|
||||
try {
|
||||
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));
|
||||
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);
|
||||
|
||||
return languageLoader.loadMod(info, modFile.getScanResult(), FMLLoader.getGameLayer());
|
||||
} catch (ModLoadingException mle) {
|
||||
// exceptions are caught and added to the error list for later handling
|
||||
loadingExceptions.add(mle);
|
||||
LOADING_EXCEPTIONS.add(mle);
|
||||
// return an errored container instance here, because we tried and failed building a container.
|
||||
return new ErroredModContainer();
|
||||
}
|
||||
@@ -303,60 +341,67 @@ public 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 get().loadingStateValid;
|
||||
return loadingStateValid;
|
||||
}
|
||||
|
||||
public boolean hasCompletedState(final String stateName) {
|
||||
IModLoadingState state = stateManager.findState(stateName);
|
||||
return completedStates.contains(state);
|
||||
public static boolean hasCompletedState(IModLoadingState state) {
|
||||
return COMPLETED_STATES.contains(state);
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
|
||||
public static <T extends IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event generator to a broken mod state");
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(generator.apply(mc)));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(generator.apply(mod));
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void postEvent(T e) {
|
||||
public static <T extends IModBusEvent> void postEvent(T e) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(e);
|
||||
}
|
||||
public <T extends Event & IModBusEvent> T postEventWithReturn(T e) {
|
||||
|
||||
public static <T extends IModBusEvent> T postEventWithReturn(T e) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return e;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(e);
|
||||
return e;
|
||||
}
|
||||
public <T extends Event & IModBusEvent> void postEventWrapContainerInModOrder(T event) {
|
||||
postEventWithWrapInModOrder(event, (mc, e) -> ModLoadingContext.get().setActiveContainer(mc), (mc, e) -> ModLoadingContext.get().setActiveContainer(null));
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static <T extends IModBusEvent> void postEventWrapContainerInModOrder(T event) {
|
||||
postEventWithWrapInModOrder(event,
|
||||
(mc, _) -> ModLoadingContext.get().setActiveContainer(mc),
|
||||
(_, _) -> ModLoadingContext.get().setActiveContainer(null)
|
||||
);
|
||||
}
|
||||
public <T extends Event & IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
|
||||
|
||||
public static <T extends 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;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> {
|
||||
pre.accept(mc, e);
|
||||
mc.acceptEvent(e);
|
||||
post.accept(mc, e);
|
||||
});
|
||||
for (var mod : ModList.getLoadedMods()) {
|
||||
pre.accept(mod, e);
|
||||
mod.acceptEvent(e);
|
||||
post.accept(mod, e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ModLoadingWarning> getWarnings()
|
||||
{
|
||||
return ImmutableList.copyOf(this.loadingWarnings);
|
||||
public static List<ModLoadingWarning> getWarnings() {
|
||||
return List.copyOf(LOADING_WARNINGS);
|
||||
}
|
||||
|
||||
public void addWarning(ModLoadingWarning warning)
|
||||
{
|
||||
this.loadingWarnings.add(warning);
|
||||
public static void addWarning(ModLoadingWarning warning) {
|
||||
LOADING_WARNINGS.add(warning);
|
||||
}
|
||||
|
||||
private static boolean runningDataGen = false;
|
||||
@@ -365,7 +410,7 @@ public class ModLoader
|
||||
return runningDataGen;
|
||||
}
|
||||
|
||||
private static class ErroredModContainer extends ModContainer {
|
||||
private static final class ErroredModContainer extends ModContainer {
|
||||
public ErroredModContainer() {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -8,54 +8,47 @@ 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 org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.BiPredicate;
|
||||
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.
|
||||
* @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")
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public static ModLoadingContext get() {
|
||||
return context.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Going to be moved to ForgeHooks for internal use.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.21.1")
|
||||
public void setActiveContainer(final ModContainer container) {
|
||||
void setActiveContainer(final ModContainer container) {
|
||||
this.activeContainer = container;
|
||||
this.languageExtension = container == null ? null : container.contextExtension.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Going to be moved to ForgeHooks for internal use.
|
||||
* @deprecated Override/Use {@link ModLoadingContext#getContainer()}
|
||||
* @deprecated Going to be moved to ForgeHooks for Internal use.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "1.21.1")
|
||||
public ModContainer getActiveContainer() {
|
||||
return activeContainer == null ? ModList.get().getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!")) : activeContainer;
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true, since = "1.21.1")
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public String getActiveNamespace() {
|
||||
return activeContainer == null ? "minecraft" : activeContainer.getNamespace();
|
||||
}
|
||||
|
||||
public ModContainer getContainer() {
|
||||
return getActiveContainer();
|
||||
return activeContainer == null
|
||||
? ModList.getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!"))
|
||||
: activeContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,8 +100,7 @@ public class ModLoadingContext
|
||||
}
|
||||
|
||||
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());
|
||||
return;
|
||||
@@ -118,8 +110,7 @@ public class ModLoadingContext
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
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.ToIntFunction;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link IModLoadingState} interface.
|
||||
@@ -26,20 +25,26 @@ import java.util.function.ToIntFunction;
|
||||
* @param inlineRunnable an optional runnable, which runs before starting the transition from this state to the next
|
||||
* @param transition optional state transition information
|
||||
*/
|
||||
public record ModLoadingState(String name, String previous,
|
||||
Function<ModList, String> message,
|
||||
ToIntFunction<ModList> size,
|
||||
ModLoadingPhase phase,
|
||||
Optional<Consumer<ModList>> inlineRunnable,
|
||||
Optional<IModStateTransition> transition) implements IModLoadingState {
|
||||
@NullMarked
|
||||
public record ModLoadingState(
|
||||
String name,
|
||||
String previous,
|
||||
Supplier<String> message,
|
||||
IntSupplier size,
|
||||
ModLoadingPhase phase,
|
||||
@Nullable Runnable inlineRunnable,
|
||||
@Nullable IModStateTransition transition
|
||||
) implements IModLoadingState {
|
||||
@Override
|
||||
public <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) {
|
||||
return transition.map(t -> t.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask));
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,9 +54,12 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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, ml -> "", f->0, phase, Optional.empty(), Optional.empty());
|
||||
return new ModLoadingState(name, previous, () -> "", () -> 0, phase, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,11 +70,13 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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
|
||||
* @return a mod loading state with state transition information and a default message
|
||||
*
|
||||
* @deprecated Use the builder
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase,
|
||||
final IModStateTransition transition) {
|
||||
return new ModLoadingState(name, previous, ml -> "Processing transition " + name, ModList::size, phase, Optional.empty(), Optional.of(transition));
|
||||
return of(name, phase).after(previous).withTransition(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,11 +88,14 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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 Function<ModList, String> message, final ModLoadingPhase phase,
|
||||
final Supplier<String> message, final ModLoadingPhase phase,
|
||||
final IModStateTransition transition) {
|
||||
return new ModLoadingState(name, previous, message, ModList::size, phase, Optional.empty(), Optional.of(transition));
|
||||
return of(name, phase).after(previous).message(message).withTransition(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,9 +107,75 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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 Consumer<ModList> inline) {
|
||||
return new ModLoadingState(name, previous, ml -> "Processing work " + name, ml->0, phase, Optional.of(inline), Optional.empty());
|
||||
final Runnable inline) {
|
||||
return of(name, phase).after(previous).withInline(inline);
|
||||
}
|
||||
}
|
||||
|
||||
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,41 +5,27 @@
|
||||
|
||||
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;
|
||||
|
||||
public class ModLoadingWarning {
|
||||
/**
|
||||
* Mod Info for mod with warning
|
||||
*/
|
||||
private final IModInfo modInfo;
|
||||
/**
|
||||
* The stage where this warning was encountered
|
||||
*/
|
||||
private final ModLoadingStage warningStage;
|
||||
|
||||
/**
|
||||
* I18N message to use for display
|
||||
*/
|
||||
private final String i18nMessage;
|
||||
|
||||
/**
|
||||
* Context for message display
|
||||
*/
|
||||
private final List<Object> context;
|
||||
|
||||
/**
|
||||
* @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 = modInfo;
|
||||
this.warningStage = warningStage;
|
||||
this.i18nMessage = i18nMessage;
|
||||
this.context = Arrays.asList(context);
|
||||
this(modInfo, warningStage, i18nMessage, List.of(context));
|
||||
}
|
||||
|
||||
public ModLoadingWarning {
|
||||
context = List.copyOf(context);
|
||||
}
|
||||
|
||||
public String formatToString() {
|
||||
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
|
||||
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Stream.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,19 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class ModStateManager {
|
||||
static ModStateManager INSTANCE;
|
||||
private final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
|
||||
public final class ModStateManager {
|
||||
private ModStateManager() {}
|
||||
private static final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
|
||||
|
||||
public ModStateManager() {
|
||||
INSTANCE = this;
|
||||
static {
|
||||
final var sp = ServiceLoader.load(FMLLoader.getGameLayer(), IModStateProvider.class);
|
||||
this.stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, sce->{})
|
||||
stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, _ -> {})
|
||||
.map(IModStateProvider::getAllStates)
|
||||
.<IModLoadingState>mapMulti(Iterable::forEach)
|
||||
.collect(Collectors.groupingBy(IModLoadingState::phase, ()->new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.groupingBy(IModLoadingState::phase, () -> new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
|
||||
}
|
||||
|
||||
public List<IModLoadingState> getStates(final ModLoadingPhase phase) {
|
||||
public static List<IModLoadingState> getStates(final ModLoadingPhase phase) {
|
||||
var nodes = stateMap.get(phase);
|
||||
var lookup = nodes.stream().collect(Collectors.toMap(IModLoadingState::name, Function.identity()));
|
||||
|
||||
@@ -36,16 +35,20 @@ public class ModStateManager {
|
||||
var dummy = ModLoadingState.empty("", "", phase);
|
||||
nodes.forEach(graph::addNode);
|
||||
graph.addNode(dummy);
|
||||
nodes.forEach(n->graph.putEdge(lookup.getOrDefault(n.previous(), dummy), n));
|
||||
for (IModLoadingState node : nodes) {
|
||||
graph.putEdge(lookup.getOrDefault(node.previous(), dummy), node);
|
||||
}
|
||||
return TopologicalSort.topologicalSort(graph, Comparator.comparingInt(nodes::indexOf)).stream().filter(st->st!=dummy).toList();
|
||||
}
|
||||
|
||||
public IModLoadingState findState(final String stateName) {
|
||||
return stateMap.values()
|
||||
public static IModLoadingState findState(final String stateName) {
|
||||
var result = stateMap.values()
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.flatMap(List::stream)
|
||||
.filter(mls -> mls.name().equals(stateName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown IModLoadingState: " + stateName));
|
||||
.orElse(null);
|
||||
if (result != null) return result;
|
||||
else throw new IllegalArgumentException("Unknown IModLoadingState: " + stateName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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((_, _)->null).thenApply(_ -> 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((_, 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,30 +14,20 @@ import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static net.minecraftforge.fml.Logging.LOADING;
|
||||
|
||||
public class ModWorkManager {
|
||||
public final 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();
|
||||
|
||||
default void drive(Runnable ticker) {
|
||||
if (!selfDriven()) {
|
||||
ticker.run();
|
||||
while (true) {
|
||||
if (!driveOne()) break;
|
||||
}
|
||||
} else {
|
||||
// park for a bit so other threads can schedule
|
||||
LockSupport.parkNanos(PARK_TIME);
|
||||
}
|
||||
}
|
||||
public sealed interface DrivenExecutor extends Executor {
|
||||
void drive(Runnable ticker);
|
||||
}
|
||||
private static class SyncExecutor implements DrivenExecutor {
|
||||
private ConcurrentLinkedDeque<Runnable> tasks = new ConcurrentLinkedDeque<>();
|
||||
|
||||
@Override
|
||||
public boolean driveOne() {
|
||||
private record SyncExecutor(ConcurrentLinkedDeque<Runnable> tasks) implements DrivenExecutor {
|
||||
public SyncExecutor() {
|
||||
this(new ConcurrentLinkedDeque<>());
|
||||
}
|
||||
|
||||
private boolean driveOne() {
|
||||
final Runnable task = tasks.pollFirst();
|
||||
if (task != null) {
|
||||
task.run();
|
||||
@@ -45,45 +35,36 @@ public class ModWorkManager {
|
||||
return task != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selfDriven() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Runnable command) {
|
||||
tasks.addLast(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drive(Runnable ticker) {
|
||||
ticker.run();
|
||||
while (true) {
|
||||
if (!driveOne()) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private record WrappingExecutor(Executor wrapped) implements DrivenExecutor {
|
||||
@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 SyncExecutor syncExecutor;
|
||||
private static final SyncExecutor syncExecutor = new SyncExecutor();
|
||||
|
||||
public static DrivenExecutor syncExecutor() {
|
||||
if (syncExecutor == null)
|
||||
syncExecutor = new SyncExecutor();
|
||||
return syncExecutor;
|
||||
}
|
||||
|
||||
@@ -91,14 +72,8 @@ public class ModWorkManager {
|
||||
return new WrappingExecutor(executor);
|
||||
}
|
||||
|
||||
private static ForkJoinPool parallelThreadPool;
|
||||
public static Executor parallelExecutor() {
|
||||
if (parallelThreadPool == null) {
|
||||
final int loadingThreadCount = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.MAX_THREADS);
|
||||
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
|
||||
parallelThreadPool = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
|
||||
}
|
||||
return parallelThreadPool;
|
||||
return LazyInit.PARALLEL_EXECUTOR;
|
||||
}
|
||||
|
||||
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
|
||||
@@ -109,4 +84,14 @@ public 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 OptionalMod<?> EMPTY = new OptionalMod<>(true);
|
||||
private static final 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.get().<T>getModObjectById(this.modId).orElse(null);
|
||||
this.value = ModList.<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) {
|
||||
return Objects.equals(((OptionalMod)obj).modId, modId);
|
||||
if (obj instanceof OptionalMod<?> optionalMod) {
|
||||
return Objects.equals(optionalMod.modId, modId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import java.util.concurrent.Executor;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
public enum ThreadSelector implements BinaryOperator<Executor> {
|
||||
SYNC((sync, parallel) -> sync),
|
||||
PARALLEL((sync, parallel) -> parallel);
|
||||
SYNC((sync, _) -> sync),
|
||||
PARALLEL((_, parallel) -> parallel);
|
||||
|
||||
private final BinaryOperator<Executor> selector;
|
||||
|
||||
|
||||
@@ -93,8 +93,8 @@ public class VersionChecker {
|
||||
return;
|
||||
}
|
||||
|
||||
var mc = ModList.get().getModContainerById("minecraft");
|
||||
var forge = ModList.get().getModContainerById("forge");
|
||||
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")
|
||||
@@ -223,7 +223,7 @@ public class VersionChecker {
|
||||
// Gather a list of mods that have opted in to this update system by providing a URL.
|
||||
private static List<IModInfo> gatherMods() {
|
||||
List<IModInfo> ret = new LinkedList<>();
|
||||
for (IModInfo info : ModList.get().getMods()) {
|
||||
for (IModInfo info : ModList.getMods()) {
|
||||
if (info.getUpdateURL().isPresent())
|
||||
ret.add(info);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -25,9 +26,66 @@ import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;
|
||||
|
||||
public class ConfigFileTypeHandler {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
|
||||
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;
|
||||
}
|
||||
|
||||
public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {
|
||||
return (c) -> {
|
||||
final Path configPath = configBasePath.resolve(c.getFileName());
|
||||
@@ -37,7 +95,7 @@ public class ConfigFileTypeHandler {
|
||||
onFileNotFound((newfile, configFormat)-> setupConfigFile(c, newfile, configFormat)).
|
||||
writingMode(WritingMode.REPLACE).
|
||||
build();
|
||||
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath.toString());
|
||||
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath);
|
||||
try
|
||||
{
|
||||
configData.load();
|
||||
@@ -46,13 +104,9 @@ public class ConfigFileTypeHandler {
|
||||
{
|
||||
throw new ConfigLoadingException(c, ex);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
return configData;
|
||||
};
|
||||
}
|
||||
@@ -60,13 +114,13 @@ public class ConfigFileTypeHandler {
|
||||
public void unload(Path configBasePath, ModConfig config) {
|
||||
Path configPath = configBasePath.resolve(config.getFileName());
|
||||
try {
|
||||
FileWatcher.defaultInstance().removeWatch(configBasePath.resolve(config.getFileName()));
|
||||
this.getWatcher().removeWatch(configPath);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to remove config {} from tracker!", configPath.toString(), e);
|
||||
LOGGER.error("Failed to remove config {} from tracker!", configPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
|
||||
private static boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
|
||||
if (!Files.isDirectory(file.getParent())) {
|
||||
Files.createDirectories(file.getParent());
|
||||
}
|
||||
@@ -113,17 +167,16 @@ public class ConfigFileTypeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ConfigFileTypeHandler[" + (type != null ? type : "UNKNOWN") + "]";
|
||||
}
|
||||
|
||||
private record ConfigWatcher(
|
||||
ModConfig modConfig,
|
||||
CommentedFileConfig commentedFileConfig,
|
||||
ClassLoader realClassLoader
|
||||
) implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
// Force the regular classloader onto the special thread
|
||||
|
||||
@@ -8,6 +8,8 @@ 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;
|
||||
@@ -16,46 +18,60 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ConfigTracker {
|
||||
public final class ConfigTracker {
|
||||
private ConfigTracker() {}
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
static final Marker CONFIG = MarkerFactory.getMarker("CONFIG");
|
||||
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;
|
||||
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<>();
|
||||
|
||||
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());
|
||||
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());
|
||||
throw new RuntimeException("Config conflict detected!");
|
||||
}
|
||||
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);
|
||||
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);
|
||||
LOGGER.debug(CONFIG, "Config file {} for {} tracking", config.getFileName(), config.getModId());
|
||||
}
|
||||
|
||||
public void loadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
public static void loadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
LOGGER.debug(CONFIG, "Loading configs type {}", type);
|
||||
this.configSets.get(type).forEach(config -> openConfig(config, configBasePath));
|
||||
for (ModConfig config : CONFIG_SETS.get(type)) {
|
||||
openConfig(config, configBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadConfigs(ModConfig.Type type, Path 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) {
|
||||
LOGGER.debug(CONFIG, "Unloading configs type {}", type);
|
||||
this.configSets.get(type).forEach(config -> closeConfig(config, configBasePath));
|
||||
for (ModConfig config : CONFIG_SETS.get(type)) {
|
||||
closeConfig(config, configBasePath);
|
||||
}
|
||||
ConfigFileTypeHandler.get(type).stopWatcher();
|
||||
}
|
||||
|
||||
private void openConfig(final ModConfig config, final Path 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) {
|
||||
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);
|
||||
@@ -63,7 +79,7 @@ public class ConfigTracker {
|
||||
config.save();
|
||||
}
|
||||
|
||||
private void closeConfig(final ModConfig config, final Path configBasePath) {
|
||||
private static 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
|
||||
@@ -76,8 +92,8 @@ public class ConfigTracker {
|
||||
}
|
||||
}
|
||||
|
||||
public void loadDefaultServerConfigs() {
|
||||
configSets.get(ModConfig.Type.SERVER).forEach(modConfig -> {
|
||||
public static void loadDefaultServerConfigs() {
|
||||
CONFIG_SETS.get(ModConfig.Type.SERVER).forEach(modConfig -> {
|
||||
final CommentedConfig commentedConfig = CommentedConfig.inMemory();
|
||||
modConfig.getSpec().correct(commentedConfig);
|
||||
modConfig.setConfigData(commentedConfig);
|
||||
@@ -85,16 +101,16 @@ public class ConfigTracker {
|
||||
});
|
||||
}
|
||||
|
||||
public String getConfigFileName(String modId, ModConfig.Type type) {
|
||||
return Optional.ofNullable(configsByMod.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
|
||||
public static String getConfigFileName(String modId, ModConfig.Type type) {
|
||||
return Optional.ofNullable(CONFIGS_BY_MOD.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
|
||||
map(ModConfig::getFullPath).map(Object::toString).orElse(null);
|
||||
}
|
||||
|
||||
public Map<ModConfig.Type, Set<ModConfig>> configSets() {
|
||||
return configSets;
|
||||
public static Map<ModConfig.Type, Set<ModConfig>> configSets() {
|
||||
return CONFIG_SETS;
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, ModConfig> fileMap() {
|
||||
return fileMap;
|
||||
public static ConcurrentHashMap<String, ModConfig> fileMap() {
|
||||
return FILE_MAP;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
package net.minecraftforge.fml.config;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.event.InheritableEvent;
|
||||
import net.minecraftforge.fml.Bindings;
|
||||
|
||||
import java.util.function.Function;
|
||||
@@ -13,6 +13,7 @@ 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();
|
||||
@@ -29,7 +30,7 @@ public interface IConfigEvent {
|
||||
ModConfig getConfig();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T extends Event & IConfigEvent> T self() {
|
||||
default <T extends InheritableEvent & IConfigEvent> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import net.minecraftforge.fml.loading.StringUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class ModConfig
|
||||
{
|
||||
@@ -24,15 +22,14 @@ 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.TOML;
|
||||
ConfigTracker.INSTANCE.trackConfig(this);
|
||||
this.configHandler = ConfigFileTypeHandler.get(type);
|
||||
ConfigTracker.trackConfig(this);
|
||||
}
|
||||
|
||||
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer activeContainer) {
|
||||
@@ -41,8 +38,9 @@ public class ModConfig
|
||||
|
||||
private static String defaultConfigName(Type type, String modId) {
|
||||
// config file name would be "forge-client.toml" and "forge-server.toml"
|
||||
return String.format(Locale.ROOT, "%s-%s.toml", modId, type.extension());
|
||||
return modId + '-' + type.extension() + ".toml";
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,33 @@
|
||||
|
||||
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 events dispatched on the ModLifecycle event bus instead of the primary event bus
|
||||
* 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.
|
||||
*/
|
||||
public interface IModBusEvent {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@ package net.minecraftforge.fml.util.thread;
|
||||
|
||||
import net.minecraftforge.fml.LogicalSide;
|
||||
|
||||
public class EffectiveSide
|
||||
{
|
||||
/**
|
||||
* 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 static LogicalSide get() {
|
||||
final ThreadGroup group = Thread.currentThread().getThreadGroup();
|
||||
return group instanceof SidedThreadGroup ? ((SidedThreadGroup) group).getSide() : LogicalSide.CLIENT;
|
||||
|
||||
@@ -7,11 +7,13 @@ 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);
|
||||
|
||||
private SidedThreadGroups() {
|
||||
public static SidedThreadGroup get(boolean client) {
|
||||
return client ? CLIENT : SERVER;
|
||||
}
|
||||
|
||||
private SidedThreadGroups() { }
|
||||
}
|
||||
@@ -28,7 +28,7 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.nulls)
|
||||
compileOnly(libs.jetbrains.annotations)
|
||||
implementation(project(':fmlloader'))
|
||||
implementation(project(':fmlcore'))
|
||||
implementation(libs.bundles.lwjgl)
|
||||
|
||||
+50
-46
@@ -22,9 +22,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
@@ -48,8 +46,6 @@ 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.IntSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -72,7 +68,7 @@ import static org.lwjgl.opengl.GL32C.*;
|
||||
* 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}, {3,2}};
|
||||
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);
|
||||
|
||||
@@ -87,11 +83,17 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
private ScheduledFuture<?> initializationFuture;
|
||||
|
||||
private PerformanceInfo performanceInfo;
|
||||
@SuppressWarnings("unused")
|
||||
private ScheduledFuture<?> performanceTick;
|
||||
// The GL ID of the window. Used for all operations
|
||||
private long window;
|
||||
// The thread that contains and ticks the window while Forge is loading mods
|
||||
private ScheduledExecutorService renderScheduler;
|
||||
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;
|
||||
@@ -150,7 +152,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
}
|
||||
this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED);
|
||||
|
||||
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + forgeVersion));
|
||||
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + FMLLoader.versionInfo().forgeVersion()));
|
||||
performanceInfo = new PerformanceInfo();
|
||||
return start(mcVersion, forgeVersion);
|
||||
}
|
||||
@@ -239,7 +241,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
|
||||
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.add(0, RenderElement.squir());
|
||||
this.elements.addFirst(RenderElement.squir());
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
@@ -259,7 +261,11 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
this.elements.removeIf(element -> !element.render(context, framecount));
|
||||
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++;
|
||||
}
|
||||
@@ -284,12 +290,6 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
* Start the window and Render Thread; we're ready to go.
|
||||
*/
|
||||
public Runnable start(@Nullable String mcVersion, final String forgeVersion) {
|
||||
renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
final var thread = Executors.defaultThreadFactory().newThread(r);
|
||||
thread.setName("EarlyDisplay");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
initWindow(mcVersion);
|
||||
this.initializationFuture = renderScheduler.schedule(() -> initRender(mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS);
|
||||
return this::periodicTick;
|
||||
@@ -301,14 +301,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
return this.glVersion;
|
||||
}
|
||||
|
||||
private void crashElegantly(String errorDetails) {
|
||||
String qrText;
|
||||
try (var is = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/glfailure.txt")))) {
|
||||
qrText = is.lines().collect(Collectors.joining("\n"));
|
||||
} catch (IOException ioe) {
|
||||
qrText = "";
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -319,17 +312,18 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
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", false);
|
||||
if (res) {
|
||||
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", false);
|
||||
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.
|
||||
*
|
||||
@@ -407,13 +401,13 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
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
|
||||
@@ -497,10 +491,6 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
glfwPollEvents();
|
||||
}
|
||||
|
||||
private void badWindowHandler(final int code, final long desc) {
|
||||
LOGGER.error("Got error from GLFW window init: "+code+ " "+MemoryUtil.memUTF8(desc));
|
||||
}
|
||||
|
||||
private void winResize(long window, int width, int height) {
|
||||
if (window == this.window && width != 0 && height != 0) {
|
||||
this.winWidth = width;
|
||||
@@ -520,7 +510,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
this.winY = y;
|
||||
}
|
||||
}
|
||||
private void handleLastGLFWError(BiConsumer<Integer, String> handler) {
|
||||
private static void handleLastGLFWError(BiConsumer<Integer, String> handler) {
|
||||
try (MemoryStack memorystack = MemoryStack.stackPush()) {
|
||||
PointerBuffer pointerbuffer = memorystack.mallocPointer(1);
|
||||
int error = glfwGetError(pointerbuffer);
|
||||
@@ -538,7 +528,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
*
|
||||
* @return the Window we own.
|
||||
*/
|
||||
public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitorSupplier) {
|
||||
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);
|
||||
@@ -558,11 +548,11 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
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");
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -572,7 +562,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
// Set the title to what the game wants
|
||||
glfwSetWindowTitle(window, title.get());
|
||||
glfwSetWindowTitle(window, title);
|
||||
glfwSwapInterval(0);
|
||||
// Clean up our hooks
|
||||
glfwSetFramebufferSizeCallback(window, null).free();
|
||||
@@ -612,10 +602,25 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
|
||||
@Override
|
||||
public void updateModuleReads(final ModuleLayer layer) {
|
||||
var fm = layer.findModule("forge").orElseThrow();
|
||||
getClass().getModule().addReads(fm);
|
||||
var clz = FMLLoader.getGameLayer().findModule("forge").map(l->Class.forName(l, "net.minecraftforge.client.loading.ForgeLoadingOverlay")).orElseThrow();
|
||||
loadingOverlay = Arrays.stream(clz.getDeclaredMethods()).filter(m-> Modifier.isStatic(m.getModifiers()) && m.getName().equals("newInstance")).findFirst().orElseThrow();
|
||||
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() {
|
||||
@@ -633,8 +638,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
|
||||
}
|
||||
|
||||
public void addMojangTexture(final int textureId) {
|
||||
this.elements.add(0, RenderElement.mojang(textureId, framecount));
|
||||
// this.elements.get(0).retire(framecount + 1);
|
||||
this.elements.addFirst(RenderElement.mojang(textureId, framecount));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
+37
-46
@@ -22,8 +22,8 @@ public class RenderElement {
|
||||
static int globalAlpha = 255;
|
||||
private int retireCount;
|
||||
|
||||
@FunctionalInterface
|
||||
interface Renderer {
|
||||
|
||||
void accept(SimpleBufferBuilder bb, DisplayContext context, int frame);
|
||||
|
||||
default Renderer then(Renderer r) {
|
||||
@@ -38,6 +38,9 @@ public class RenderElement {
|
||||
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> {}
|
||||
|
||||
@@ -56,11 +59,18 @@ public class RenderElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link RenderElement#RenderElement(Renderer)} instead */
|
||||
@Deprecated(since = "1.21.5", forRemoval = true)
|
||||
public RenderElement(final Initializer rendererInitializer) {
|
||||
this.bb = new SimpleBufferBuilder(1);
|
||||
this.renderer = rendererInitializer.get();
|
||||
}
|
||||
|
||||
public RenderElement(Renderer renderer) {
|
||||
this.bb = new SimpleBufferBuilder(1);
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
public boolean render(DisplayContext ctx, int count) {
|
||||
this.renderer.accept(bb, ctx, count);
|
||||
return this.retireCount == 0 || this.retireCount < count;
|
||||
@@ -75,7 +85,7 @@ public class RenderElement {
|
||||
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 = clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f);
|
||||
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;
|
||||
@@ -84,19 +94,9 @@ public class RenderElement {
|
||||
|
||||
font.generateVerticesForTexts(10, context.scaledHeight() - texts.size() * font.lineSpacing() + font.descent() - 10, bb, texts.toArray(SimpleFont.DisplayText[]::new));
|
||||
}
|
||||
public static RenderElement monag() {
|
||||
return new RenderElement(RenderElement.initializeTexture("monagstudios.png", 45000, 4, (bb, ctx, sz, frame) -> {
|
||||
var size = 256;
|
||||
var x0 = (ctx.width() - 2 * size) / 2;
|
||||
var y0 = 64;
|
||||
QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, 0xFFFFFFFF);
|
||||
QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, 0xFFFFFFFF);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
public static RenderElement mojang(final int textureId, final int frameStart) {
|
||||
return new RenderElement(()->(bb, ctx, frame) -> {
|
||||
return new RenderElement((bb, ctx, frame) -> {
|
||||
var size = 256 * ctx.scale();
|
||||
var x0 = (ctx.scaledWidth() - 2 * size) / 2;
|
||||
var y0 = 64 * ctx.scale() + 32;
|
||||
@@ -116,7 +116,7 @@ public class RenderElement {
|
||||
}
|
||||
|
||||
public static RenderElement forgeVersionOverlay(SimpleFont font, String version) {
|
||||
return new RenderElement(RenderElement.initializeText(font, (bb, fnt, ctx)->
|
||||
return new RenderElement(RenderElement.initializeText(font, (bb, _, 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)))));
|
||||
@@ -148,11 +148,11 @@ public class RenderElement {
|
||||
}));
|
||||
}
|
||||
public static RenderElement progressBars(SimpleFont font) {
|
||||
return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame));
|
||||
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));
|
||||
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) {
|
||||
@@ -178,19 +178,19 @@ public class RenderElement {
|
||||
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));
|
||||
bar = progressBar(ctx->new int[] {(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, _->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()});
|
||||
bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, _ -> colour, _ -> 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);
|
||||
Renderer label = (bb, ctx, _) -> 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};
|
||||
return new float[] { 0f, 1f };
|
||||
} else {
|
||||
var progress = frame % 100;
|
||||
return new float[]{clamp((progress - 2) / 100f, 0f, 1f), clamp((progress + 2) / 100f, 0f, 1f)};
|
||||
int progress = frame % 100;
|
||||
return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.clamp((progress + 2) / 100f, 0f, 1f) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +198,9 @@ public class RenderElement {
|
||||
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 bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, _ -> colour, _ -> 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);
|
||||
Renderer label = (bb, ctx, _) -> 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);
|
||||
}
|
||||
|
||||
@@ -249,8 +249,8 @@ public class RenderElement {
|
||||
};
|
||||
}
|
||||
|
||||
private static Initializer initializeText(SimpleFont font, TextGenerator textGenerator) {
|
||||
return () -> (bb, context, frame) -> renderText(font, textGenerator, bb, context);
|
||||
private static Renderer initializeText(SimpleFont font, TextGenerator textGenerator) {
|
||||
return (bb, context, _) -> renderText(font, textGenerator, bb, context);
|
||||
}
|
||||
|
||||
private static void renderText(final SimpleFont font, final TextGenerator textGenerator, final SimpleBufferBuilder bb, final DisplayContext context) {
|
||||
@@ -262,16 +262,15 @@ public class RenderElement {
|
||||
}
|
||||
|
||||
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));
|
||||
return (bb, font, _) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour));
|
||||
}
|
||||
private static Initializer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
|
||||
return ()->{
|
||||
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 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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -281,21 +280,13 @@ public class RenderElement {
|
||||
bb.draw();
|
||||
}
|
||||
|
||||
|
||||
@Deprecated(since = "1.21.5", forRemoval = true)
|
||||
public static float clamp(float num, float min, float max) {
|
||||
if (num < min) {
|
||||
return min;
|
||||
} else {
|
||||
return Math.min(num, max);
|
||||
}
|
||||
return Math.clamp(num, min, max);
|
||||
}
|
||||
|
||||
public static int clamp(int num, int min, int max) {
|
||||
if (num < min) {
|
||||
return min;
|
||||
} else {
|
||||
return Math.min(num, max);
|
||||
}
|
||||
return Math.min(max, Math.max(num, min));
|
||||
}
|
||||
|
||||
public static int hsvToRGB(float hue, float saturation, float value) {
|
||||
|
||||
@@ -11,8 +11,6 @@ import org.lwjgl.stb.STBTTFontinfo;
|
||||
import org.lwjgl.stb.STBTTPackContext;
|
||||
import org.lwjgl.stb.STBTTPackRange;
|
||||
import org.lwjgl.stb.STBTTPackedchar;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@@ -24,8 +22,8 @@ public class SimpleFont {
|
||||
private final int textureNumber;
|
||||
private final int lineSpacing;
|
||||
private final int descent;
|
||||
private final int GLYPH_COUNT = 127-32;
|
||||
private Glyph[] glyphs;
|
||||
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) {
|
||||
@@ -122,7 +120,7 @@ public class SimpleFont {
|
||||
case '\n', '\t' -> 0;
|
||||
case ' ' -> glyphs[0].charwidth();
|
||||
default -> {
|
||||
if (c - 32 < this.GLYPH_COUNT && c > 32) {
|
||||
if (c - 32 < GLYPH_COUNT && c > 32) {
|
||||
yield this.glyphs[c - 32].charwidth();
|
||||
} else {
|
||||
yield 0;
|
||||
@@ -153,7 +151,7 @@ public class SimpleFont {
|
||||
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 < font.GLYPH_COUNT && c > 32) {
|
||||
if (c-32 < GLYPH_COUNT && c > 32) {
|
||||
pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb);
|
||||
}
|
||||
yield pos;
|
||||
|
||||
+46
-6
@@ -5,6 +5,7 @@ plugins {
|
||||
id 'maven-publish'
|
||||
id 'net.minecraftforge.licenser'
|
||||
id 'net.minecraftforge.gradleutils'
|
||||
alias(libs.plugins.apt)
|
||||
}
|
||||
|
||||
apply from: rootProject.file('build_shared.gradle')
|
||||
@@ -12,7 +13,7 @@ apply from: rootProject.file('build_shared.gradle')
|
||||
configurations.forEach{ it.transitive = false }
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.nulls)
|
||||
compileOnly(libs.jetbrains.annotations)
|
||||
|
||||
api(libs.bundles.asm) // Needed by all the black magic
|
||||
api(libs.forgespi)
|
||||
@@ -25,7 +26,6 @@ dependencies {
|
||||
api(libs.apache.commons)
|
||||
api(libs.bundles.night.config)
|
||||
api(libs.modlauncher)
|
||||
api(libs.coremods)
|
||||
api(libs.mojang.logging)
|
||||
api(libs.jarjar.selector)
|
||||
api(libs.jarjar.meta)
|
||||
@@ -35,11 +35,13 @@ dependencies {
|
||||
implementation(libs.accesstransformers)
|
||||
implementation(libs.terminalconsoleappender)
|
||||
implementation(libs.jimfs)
|
||||
implementation(libs.roimfs)
|
||||
|
||||
// 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)
|
||||
|
||||
testCompileOnly(libs.nulls)
|
||||
testCompileOnly(libs.jetbrains.annotations)
|
||||
testRuntimeOnly(libs.bootstrap)
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ publishing {
|
||||
license PomUtils.Licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
@@ -101,6 +103,44 @@ tasks.register('writeForgeVersionJson') {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('generateResources').configure {
|
||||
dependsOn('writeForgeVersionJson')
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-34
@@ -34,11 +34,10 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
||||
* }</code>
|
||||
* </pre>
|
||||
*/
|
||||
public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
|
||||
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.
|
||||
private static final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; // Don't directly reference this to prevent class loading.
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
@@ -49,56 +48,48 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
|
||||
|
||||
@Override
|
||||
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
|
||||
{
|
||||
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 YAY;
|
||||
return internalName.contains("$") ? YAY : NAY; // Anonymous subclasses only
|
||||
}
|
||||
|
||||
@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))
|
||||
{
|
||||
Holder cls = new Holder();
|
||||
} else if (CAP_INJECT.equals(classNode.superName)) {
|
||||
var cls = new Object() { String value = null; };
|
||||
|
||||
SignatureReader reader = new SignatureReader(classNode.signature); // Having a node version of this would probably be useful.
|
||||
reader.accept(new SignatureVisitor(Opcodes.ASM9)
|
||||
{
|
||||
reader.accept(new SignatureVisitor(Opcodes.ASM9) {
|
||||
final 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;
|
||||
@@ -113,15 +104,8 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
mtd.visitInsn(Opcodes.ARETURN);
|
||||
mtd.visitEnd();
|
||||
return ComputeFlags.COMPUTE_MAXS;
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
return ComputeFlags.NO_REWRITE;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Holder {
|
||||
String value;
|
||||
}
|
||||
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* 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.Set;
|
||||
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 static final Set<String> VANILLA_OBJECT_HOLDER_CLASSES = VANILLA_OBJECT_HOLDERS.keySet().stream()
|
||||
.map(it -> it.replace('.', '/'))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
private static final String OBJECT_HOLDER = "Lnet/minecraftforge/registries/ObjectHolder;"; //Don't directly reference this to prevent class loading.
|
||||
private static final int PUBLIC_STATIC_FINAL_FLAGS = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL;
|
||||
|
||||
@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) {
|
||||
if (isEmpty)
|
||||
return NAY;
|
||||
|
||||
// Let's skip processing classes that definitely don't have object holders
|
||||
String internalName = classType.getInternalName();
|
||||
|
||||
// Forge classes that aren't in the debug package (that package is used for tests)
|
||||
if (internalName.startsWith("net/minecraftforge/") && !internalName.substring(18).contains("debug"))
|
||||
return NAY;
|
||||
|
||||
// Vanilla classes that don't have object holders
|
||||
if (internalName.startsWith("com/mojang/"))
|
||||
return NAY;
|
||||
|
||||
// ...except specific ones we added to the VANILLA_OBJECT_HOLDERS map
|
||||
if (internalName.startsWith("net/minecraft/") && !VANILLA_OBJECT_HOLDER_CLASSES.contains(internalName))
|
||||
return NAY;
|
||||
|
||||
return YAY;
|
||||
}
|
||||
|
||||
private boolean hasHolder(List<AnnotationNode> lst) {
|
||||
return lst != null && lst.stream().anyMatch(n -> n.desc.equals(OBJECT_HOLDER));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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
|
||||
|
||||
//Fix Annotated Fields before injecting from class level
|
||||
classNode.fields.stream().filter(f -> ((f.access & PUBLIC_STATIC_FINAL_FLAGS) == PUBLIC_STATIC_FINAL_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 & PUBLIC_STATIC_FINAL_FLAGS) == PUBLIC_STATIC_FINAL_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;
|
||||
@@ -47,16 +47,21 @@ 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)
|
||||
if (isEmpty || !LazyInit.ANY_MODS)
|
||||
return NAY;
|
||||
|
||||
String internalName = classType.getInternalName();
|
||||
if (internalName.startsWith("net/minecraftforge/") || internalName.startsWith("com/mojang/"))
|
||||
return NAY;
|
||||
if (internalName.startsWith("net/minecraft/client/model") || internalName.startsWith("net/minecraft/world"))
|
||||
return YAY;
|
||||
|
||||
return YAY;
|
||||
return NAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
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 final 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 {
|
||||
|
||||
@@ -109,7 +109,7 @@ public class FMLConfig
|
||||
}
|
||||
if (!configSpec.isCorrect(configData)) {
|
||||
LOGGER.warn(CORE, "Configuration file {} is not correct. Correcting", configFile);
|
||||
configSpec.correct(configData, (action, path, incorrectValue, correctedValue) ->
|
||||
configSpec.correct(configData, (_, path, incorrectValue, correctedValue) ->
|
||||
LOGGER.info(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue));
|
||||
}
|
||||
configData.putAllComments(configComments);
|
||||
|
||||
@@ -20,8 +20,8 @@ public class FMLEnvironment
|
||||
public static final boolean secureJarsEnabled = FMLLoader.isSecureJarEnabled();
|
||||
|
||||
static void setupInteropEnvironment(IEnvironment environment) {
|
||||
environment.computePropertyIfAbsent(IEnvironment.Keys.NAMING.get(), v->naming);
|
||||
environment.computePropertyIfAbsent(Environment.Keys.DIST.get(), v->dist);
|
||||
environment.computePropertyIfAbsent(IEnvironment.Keys.NAMING.get(), _->naming);
|
||||
environment.computePropertyIfAbsent(Environment.Keys.DIST.get(), _->dist);
|
||||
}
|
||||
|
||||
public static class Keys {
|
||||
|
||||
@@ -16,12 +16,9 @@ import net.minecraftforge.accesstransformer.service.AccessTransformerService;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
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.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -33,15 +30,11 @@ import java.util.function.BiFunction;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.SCAN;
|
||||
|
||||
public class FMLLoader {
|
||||
public final class FMLLoader {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static AccessTransformerService accessTransformer;
|
||||
private static ModDiscoverer modDiscoverer;
|
||||
private static ICoreModProvider coreModProvider;
|
||||
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();
|
||||
@@ -57,15 +50,9 @@ public class FMLLoader {
|
||||
LOGGER.debug(CORE, "Detected version data : {}", versionInfo);
|
||||
LOGGER.debug(CORE, "FML {} loading", LauncherVersion.getVersion());
|
||||
|
||||
// Allows us to communicate properties with other services through ModLauncher
|
||||
setupBlackboardKeys();
|
||||
|
||||
checkPackage(ITransformationService.class, "4.0", "ModLauncher");
|
||||
accessTransformer = getPlugin(env, "accesstransformer", "1.0", "AccessTransformer");
|
||||
/*eventBus =*/ getPlugin(env, "eventbus", "1.0", "EventBus");
|
||||
runtimeDistCleaner = getPlugin(env, "runtimedistcleaner", "1.0", "RuntimeDistCleaner");
|
||||
coreModProvider = getSingleService(ICoreModProvider.class, "CoreMod");
|
||||
LOGGER.debug(CORE,"FML found CoreMod version : {}", JarVersionLookupHandler.getInfo(coreModProvider.getClass()).impl().version().orElse("MISSING"));
|
||||
checkPackage(Environment.class, "2.0", "ForgeSPI");
|
||||
|
||||
try {
|
||||
@@ -77,14 +64,15 @@ public class FMLLoader {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getPlugin(IEnvironment env, String id, String version, String name) throws IncompatibleEnvironmentException {
|
||||
@SuppressWarnings("unchecked")
|
||||
var plugin = (T)env.findLaunchPlugin(id).orElseThrow(() -> {
|
||||
var plugin = env.findLaunchPlugin(id).orElse(null);
|
||||
if (plugin == null) {
|
||||
LOGGER.error(CORE, "{} library is missing, we need this to run", name);
|
||||
return new IncompatibleEnvironmentException("Missing " + name + ", cannot run");
|
||||
});
|
||||
throw new IncompatibleEnvironmentException("Missing " + name + ", cannot run");
|
||||
}
|
||||
checkPackage(plugin.getClass(), version, name);
|
||||
return plugin;
|
||||
return (T) plugin;
|
||||
}
|
||||
|
||||
private static void checkPackage(Class<?> cls, String version, String name) throws IncompatibleEnvironmentException {
|
||||
@@ -102,6 +90,7 @@ public class FMLLoader {
|
||||
}
|
||||
}
|
||||
|
||||
@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(); ) {
|
||||
@@ -123,12 +112,6 @@ public class FMLLoader {
|
||||
return providers.get(0);
|
||||
}
|
||||
|
||||
private static void setupBlackboardKeys() {
|
||||
LOGGER.debug(CORE, "Requesting CoreMods to not apply the fix for ASMAPI.findFirstInstructionBefore by default");
|
||||
var blackboardKey = TypesafeMap.Key.getOrCreate(Launcher.INSTANCE.blackboard(), "coremods.use_old_findFirstInstructionBefore", Boolean.class);
|
||||
Launcher.INSTANCE.blackboard().<Boolean>computeIfAbsent(blackboardKey, k -> true);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -146,51 +129,37 @@ public class FMLLoader {
|
||||
}
|
||||
commonLaunchHandler = (CommonLaunchHandler)launchHandler.get();
|
||||
launchHandlerName = launchHandler.get().name();
|
||||
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Paths.get(".").toAbsolutePath());
|
||||
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Path.of(".").toAbsolutePath());
|
||||
|
||||
naming = commonLaunchHandler.getNaming();
|
||||
dist = commonLaunchHandler.getDist();
|
||||
production = commonLaunchHandler.isProduction();
|
||||
|
||||
accessTransformer.getExtension().accept(Pair.of(naming, "srg"));
|
||||
accessTransformer.getExtension().accept(Map.entry(naming, "srg"));
|
||||
|
||||
runtimeDistCleaner.getExtension().accept(dist);
|
||||
}
|
||||
|
||||
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments) {
|
||||
LOGGER.debug(SCAN,"Scanning for Mod Locators");
|
||||
modDiscoverer = new ModDiscoverer(arguments);
|
||||
modValidator = modDiscoverer.discoverMods();
|
||||
LOGGER.debug(SCAN, "Scanning for Mod Locators");
|
||||
modValidator = ModDiscoverer.discoverMods(arguments);
|
||||
var pluginResources = modValidator.getPluginResources();
|
||||
return List.of(pluginResources);
|
||||
}
|
||||
|
||||
public static List<ITransformationService.Resource> completeScan(IModuleLayerManager layerManager) {
|
||||
moduleLayerManager = layerManager;
|
||||
languageLoadingProvider = new LanguageLoadingProvider();
|
||||
LanguageLoadingProvider.reload();
|
||||
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 {}", modName.getFilePath());
|
||||
LOGGER.debug(SCAN, "Adding Access Transformer {} in {}", atPath, modName.getFilePath());
|
||||
accessTransformer.offerResource(atPath, modName.getFileName());
|
||||
}
|
||||
|
||||
@@ -204,10 +173,6 @@ public class FMLLoader {
|
||||
progressWindowTick.run();
|
||||
}
|
||||
|
||||
public static LoadingModList getLoadingModList() {
|
||||
return loadingModList;
|
||||
}
|
||||
|
||||
public static Path getGamePath() {
|
||||
return gamePath;
|
||||
}
|
||||
|
||||
@@ -7,24 +7,21 @@ package net.minecraftforge.fml.loading;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.modlauncher.api.*;
|
||||
import joptsimple.OptionSpecBuilder;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
|
||||
import net.minecraftforge.forgespi.Environment;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
|
||||
|
||||
public class FMLServiceProvider implements ITransformationService {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private Map<String, Object> arguments;
|
||||
private final Map<String, Object> arguments = new HashMap<>();
|
||||
|
||||
public FMLServiceProvider() {
|
||||
var markers = System.getProperty("forge.logging.markers", "").split(",");
|
||||
@@ -44,8 +41,7 @@ public class FMLServiceProvider implements ITransformationService {
|
||||
LOGGER.debug(CORE, "Loading configuration");
|
||||
FMLConfig.load();
|
||||
LOGGER.debug(CORE, "Preparing ModFile");
|
||||
environment.computePropertyIfAbsent(Environment.Keys.MODFILEFACTORY.get(), k->ModFile::new);
|
||||
arguments = new HashMap<>();
|
||||
environment.computePropertyIfAbsent(Environment.Keys.MODFILEFACTORY.get(), _->ModFile::new);
|
||||
LOGGER.debug(CORE, "Preparing launch handler");
|
||||
FMLLoader.setupLaunchHandler(environment, arguments);
|
||||
FMLEnvironment.setupInteropEnvironment(environment);
|
||||
@@ -68,18 +64,9 @@ public class FMLServiceProvider implements ITransformationService {
|
||||
FMLLoader.onInitialLoad(environment, otherServices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void arguments(BiFunction<String, String, OptionSpecBuilder> argumentBuilder) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void argumentValues(OptionResult option) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public @NotNull List<ITransformer> transformers() {
|
||||
LOGGER.debug(CORE, "Loading coremod transformers");
|
||||
return new ArrayList<>(FMLLoader.getCoreModProvider().getCoreModTransformers());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,8 @@ public class ImmediateWindowHandler {
|
||||
earlyProgress.label("Bootstrapping Minecraft");
|
||||
}
|
||||
|
||||
public static long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor) {
|
||||
return provider.setupMinecraftWindow(width, height, title, monitor);
|
||||
public static long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend) {
|
||||
return provider.setupMinecraftWindow(width, height, title, monitor, backend);
|
||||
}
|
||||
|
||||
public static boolean positionWindow(Optional<Object> monitor,IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) {
|
||||
@@ -104,9 +104,9 @@ public class ImmediateWindowHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor) {
|
||||
public long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend) {
|
||||
try {
|
||||
var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, monitor);
|
||||
var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, monitor, backend);
|
||||
return longsupplier.getAsLong();
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("How did you get here?", e);
|
||||
@@ -140,7 +140,7 @@ public class ImmediateWindowHandler {
|
||||
|
||||
@Override
|
||||
public void updateModuleReads(final ModuleLayer layer) {
|
||||
var fm = layer.findModule("forge");
|
||||
var fm = layer.findModule("net.minecraftforge.forge");
|
||||
if (fm.isPresent()) {
|
||||
getClass().getModule().addReads(fm.get());
|
||||
var clz = fm.map(l -> Class.forName(l, "net.minecraftforge.client.loading.NoVizFallback")).orElseThrow();
|
||||
|
||||
@@ -8,8 +8,6 @@ package net.minecraftforge.fml.loading;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@@ -66,7 +64,7 @@ public interface ImmediateWindowProvider {
|
||||
* @param monitor This is the monitor it should appear on.
|
||||
* @return The window id
|
||||
*/
|
||||
long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor);
|
||||
long setupMinecraftWindow(final int width, final int height, final String title, final long monitor, final Supplier<Object> backend);
|
||||
|
||||
/**
|
||||
* This is called after window handoff to allow us to tell Mojang about our window's position. This might give a
|
||||
|
||||
+11
-19
@@ -18,7 +18,6 @@ import org.slf4j.Logger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceConfigurationError;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.Consumer;
|
||||
@@ -28,43 +27,36 @@ import java.util.stream.Stream;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.LOADING;
|
||||
|
||||
public class LanguageLoadingProvider {
|
||||
public void forEach(Consumer<IModLanguageProvider> consumer) {
|
||||
public final class LanguageLoadingProvider {
|
||||
public static void forEach(Consumer<IModLanguageProvider> consumer) {
|
||||
providers.forEach(consumer);
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEach(Function<IModLanguageProvider, T> function) {
|
||||
public static <T> Stream<T> applyForEach(Function<IModLanguageProvider, T> function) {
|
||||
return providers.stream().map(function);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
public static void reload() {
|
||||
loadLanguageProviders();
|
||||
}
|
||||
|
||||
/** This doesn't actually do anything with the argument, so just call reload(). Not sure when cpw broke that. */
|
||||
@Deprecated(forRemoval = true, since = "1.20.2")
|
||||
public void addAdditionalLanguages(List<ModFile> modFiles) {
|
||||
reload();
|
||||
}
|
||||
|
||||
/*==========================================================================*
|
||||
* INTERNAL SHIT *
|
||||
* INTERNAL *
|
||||
*==========================================================================*/
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final ServiceLoader<IModLanguageProvider> serviceLoader;
|
||||
private final List<IModLanguageProvider> providers = new ArrayList<>();
|
||||
private final Map<String, Wrapper> providersByName = new HashMap<>();
|
||||
private static final ServiceLoader<IModLanguageProvider> serviceLoader;
|
||||
private static final ArrayList<IModLanguageProvider> providers = new ArrayList<>();
|
||||
private static final HashMap<String, Wrapper> providersByName = new HashMap<>();
|
||||
private static final DefaultArtifactVersion IN_DEV = new DefaultArtifactVersion("0.0-dev");
|
||||
|
||||
private record Wrapper(ArtifactVersion version, IModLanguageProvider service) {}
|
||||
|
||||
LanguageLoadingProvider() {
|
||||
static {
|
||||
var sl = Launcher.INSTANCE.environment().findModuleLayerManager().flatMap(lm -> lm.getLayer(Layer.PLUGIN)).orElseThrow();
|
||||
serviceLoader = ServiceLoader.load(sl, IModLanguageProvider.class);
|
||||
loadLanguageProviders();
|
||||
}
|
||||
|
||||
private void loadLanguageProviders() {
|
||||
private static void loadLanguageProviders() {
|
||||
providers.clear();
|
||||
providersByName.clear();
|
||||
|
||||
@@ -97,7 +89,7 @@ public class LanguageLoadingProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public IModLanguageProvider findLanguage(ModFile mod, String language, VersionRange versionRange) {
|
||||
public static IModLanguageProvider findLanguage(ModFile mod, String language, VersionRange versionRange) {
|
||||
var consumer = mod.getFileName();
|
||||
var wrapper = providersByName.get(language);
|
||||
|
||||
|
||||
@@ -5,167 +5,46 @@
|
||||
|
||||
package net.minecraftforge.fml.loading;
|
||||
|
||||
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.BackgroundScanHandler;
|
||||
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.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Master list of all mods <em>in the loading context. This class cannot refer outside the
|
||||
* loading package</em>
|
||||
*/
|
||||
public class LoadingModList
|
||||
{
|
||||
private static LoadingModList INSTANCE;
|
||||
private final List<ModFileInfo> modFiles;
|
||||
private final List<ModInfo> sortedList;
|
||||
private final Map<String, ModFileInfo> fileById;
|
||||
private final List<EarlyLoadingException> preLoadErrors;
|
||||
private List<IModFile> brokenFiles;
|
||||
|
||||
private LoadingModList(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(ModFileInfo::getMods)
|
||||
.flatMap(Collection::stream)
|
||||
.map(ModInfo.class::cast)
|
||||
.collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile));
|
||||
this.preLoadErrors = new ArrayList<>();
|
||||
/// Master list of all mods in the loading context
|
||||
public sealed interface LoadingModList permits LoadingModListImpl {
|
||||
static ModFileInfo getModFileById(String modId) {
|
||||
return get().fileById().get(modId);
|
||||
}
|
||||
|
||||
public static LoadingModList of(List<ModFile> modFiles, List<ModInfo> sortedList, final EarlyLoadingException earlyLoadingException)
|
||||
{
|
||||
INSTANCE = new LoadingModList(modFiles, sortedList);
|
||||
if (earlyLoadingException != null)
|
||||
{
|
||||
INSTANCE.preLoadErrors.add(earlyLoadingException);
|
||||
}
|
||||
return INSTANCE;
|
||||
static List<ModFileInfo> getModFiles() {
|
||||
return get().getModFiles();
|
||||
}
|
||||
|
||||
public static LoadingModList get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
public void addCoreMods()
|
||||
{
|
||||
modFiles.stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.map(ModFile::getCoreMods)
|
||||
.flatMap(List::stream)
|
||||
.forEach(FMLLoader.getCoreModProvider()::addCoreMod);
|
||||
static List<ModInfo> getMods() {
|
||||
return get().getMods();
|
||||
}
|
||||
|
||||
public void addAccessTransformers()
|
||||
{
|
||||
modFiles.stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.forEach(mod -> mod.getAccessTransformer().ifPresent(path -> FMLLoader.addAccessTransformer(path, mod)));
|
||||
}
|
||||
|
||||
public void addForScanning(BackgroundScanHandler backgroundScanHandler)
|
||||
{
|
||||
backgroundScanHandler.setLoadingModList(this);
|
||||
modFiles.stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.forEach(backgroundScanHandler::submitForScanning);
|
||||
}
|
||||
|
||||
public List<ModFileInfo> getModFiles()
|
||||
{
|
||||
return modFiles;
|
||||
}
|
||||
|
||||
public Path findResource(final String className)
|
||||
{
|
||||
for (ModFileInfo mf : modFiles) {
|
||||
final Path resource = mf.getFile().findResource(className);
|
||||
static @Nullable Path findResource(String className) {
|
||||
for (ModFileInfo modFileInfo : get().getModFiles()) {
|
||||
Path resource = modFileInfo.getFile().findResource(className);
|
||||
if (Files.exists(resource)) return resource;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Enumeration<URL> findAllURLsForResource(final String resName) {
|
||||
final String resourceName;
|
||||
// strip a leading slash
|
||||
if (resName.startsWith("/")) {
|
||||
resourceName = resName.substring(1);
|
||||
} else {
|
||||
resourceName = resName;
|
||||
}
|
||||
return new Enumeration<URL>() {
|
||||
private final Iterator<ModFileInfo> modFileIterator = modFiles.iterator();
|
||||
private URL next;
|
||||
@Override
|
||||
public boolean hasMoreElements() {
|
||||
if (next!=null) return true;
|
||||
next = findNextURL();
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL nextElement() {
|
||||
if (next == null) {
|
||||
next = findNextURL();
|
||||
if (next == null) throw new NoSuchElementException();
|
||||
}
|
||||
URL result = next;
|
||||
next = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
private URL findNextURL() {
|
||||
while (modFileIterator.hasNext()) {
|
||||
final ModFileInfo next = modFileIterator.next();
|
||||
final Path resource = next.getFile().findResource(resourceName);
|
||||
if (Files.exists(resource)) {
|
||||
return LamdbaExceptionUtils.uncheck(()->new URL("modjar://" + next.getMods().get(0).getModId() + "/" + resourceName));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
static List<EarlyLoadingException> getErrors() {
|
||||
return get().getErrors();
|
||||
}
|
||||
|
||||
public ModFileInfo getModFileById(String modid)
|
||||
{
|
||||
return this.fileById.get(modid);
|
||||
static List<IModFile> getBrokenFiles() {
|
||||
return get().getBrokenFiles();
|
||||
}
|
||||
|
||||
public List<ModInfo> getMods()
|
||||
{
|
||||
return this.sortedList;
|
||||
}
|
||||
|
||||
public List<EarlyLoadingException> getErrors() {
|
||||
return preLoadErrors;
|
||||
}
|
||||
|
||||
public void setBrokenFiles(final List<IModFile> brokenFiles) {
|
||||
this.brokenFiles = brokenFiles;
|
||||
}
|
||||
|
||||
public List<IModFile> getBrokenFiles() {
|
||||
return this.brokenFiles;
|
||||
private static LoadingModListImpl get() {
|
||||
return LoadingModListImpl.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.loading;
|
||||
|
||||
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
|
||||
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 java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
|
||||
/**
|
||||
* Master list of all mods <em>in the loading context. This class cannot refer outside the
|
||||
* loading package</em>
|
||||
*/
|
||||
record LoadingModListImpl(
|
||||
List<ModFileInfo> getModFiles,
|
||||
List<ModInfo> getMods,
|
||||
Map<String, ModFileInfo> fileById,
|
||||
ArrayList<EarlyLoadingException> getErrors,
|
||||
ArrayList<IModFile> getBrokenFiles
|
||||
) implements LoadingModList {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static ModSorter.State temp;
|
||||
|
||||
LoadingModListImpl(List<ModFile> modFiles, List<ModInfo> sortedList) {
|
||||
var modFileInfos = modFiles.stream()
|
||||
.map(ModFile::getModFileInfo)
|
||||
.map(ModFileInfo.class::cast)
|
||||
.toList();
|
||||
sortedList = sortedList.stream().toList();
|
||||
var fileById = modFileInfos.stream()
|
||||
.map(ModFileInfo::getMods)
|
||||
.flatMap(Collection::stream)
|
||||
.map(ModInfo.class::cast)
|
||||
.collect(Collectors.toUnmodifiableMap(ModInfo::getModId, ModInfo::getOwningFile));
|
||||
this(modFileInfos, sortedList, fileById, new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
|
||||
static LoadingModListImpl init(ModSorter.State modSorterState, EarlyLoadingException earlyLoadingException) {
|
||||
temp = modSorterState;
|
||||
var instance = get();
|
||||
if (earlyLoadingException != null)
|
||||
instance.getErrors.add(earlyLoadingException);
|
||||
temp = null;
|
||||
instance.addAccessTransformers();
|
||||
return instance;
|
||||
}
|
||||
|
||||
static LoadingModListImpl get() {
|
||||
final class LazyInit {
|
||||
private LazyInit() {}
|
||||
private static final LoadingModListImpl INSTANCE
|
||||
= new LoadingModListImpl(LoadingModListImpl.temp.files(), LoadingModListImpl.temp.mods());
|
||||
}
|
||||
return LazyInit.INSTANCE;
|
||||
}
|
||||
|
||||
private void addAccessTransformers() {
|
||||
var errors = new ArrayList<ExceptionData>();
|
||||
|
||||
for (ModFileInfo modFile : getModFiles) {
|
||||
ModFile mod = modFile.getFile();
|
||||
for (var at : mod.getAccessTransformers()) {
|
||||
if (!Files.exists(at)) {
|
||||
var message = "Invalid mod file: " + modFile.getFile().getFileName() + ". Missing Access Transformer: " + at;
|
||||
errors.add(new ExceptionData(message));
|
||||
LOGGER.error(message);
|
||||
} else
|
||||
FMLLoader.addAccessTransformer(at, mod);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty())
|
||||
getErrors.add(new EarlyLoadingException("Invalid Access Transformers", null, errors));
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package net.minecraftforge.fml.loading;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.modlauncher.api.INameMappingService;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -36,7 +35,7 @@ public class MCPNamingService implements INameMappingService {
|
||||
|
||||
@Override
|
||||
public Map.Entry<String, String> understanding() {
|
||||
return Pair.of("srg", "mcp");
|
||||
return Map.entry("srg", "mcp");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package net.minecraftforge.fml.loading;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Convert a maven coordinate into a Path.
|
||||
@@ -34,7 +33,7 @@ public class MavenCoordinateResolver {
|
||||
(!extension.isEmpty() ? "." + extension : ".jar");
|
||||
|
||||
String[] groups = groupId.split("\\.");
|
||||
Path result = Paths.get(groups[0]);
|
||||
Path result = Path.of(groups[0]);
|
||||
for (int i = 1; i < groups.length; i++) {
|
||||
result = result.resolve(groups[i]);
|
||||
}
|
||||
|
||||
+13
@@ -7,8 +7,10 @@ package net.minecraftforge.fml.loading;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.jarhandling.SecureJar;
|
||||
import cpw.mods.modlauncher.Launcher;
|
||||
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
|
||||
import cpw.mods.modlauncher.api.NamedPath;
|
||||
import cpw.mods.modlauncher.api.TypesafeMap;
|
||||
import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -40,6 +42,17 @@ public class ModDirTransformerDiscoverer implements ITransformerDiscoveryService
|
||||
|
||||
@Override
|
||||
public void earlyInitialization(final String launchTarget, final String[] arguments) {
|
||||
boolean isMixinEnabledInDev = false;
|
||||
for (String arg : arguments) {
|
||||
if (arg.startsWith("-mixin") || arg.startsWith("--mixin")) {
|
||||
isMixinEnabledInDev = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Launcher.INSTANCE.blackboard().putIfAbsent(
|
||||
TypesafeMap.Key.getOrCreate(Launcher.INSTANCE.blackboard(), "isMixinEnabledInDev", Boolean.class),
|
||||
isMixinEnabledInDev
|
||||
);
|
||||
ImmediateWindowHandler.load(launchTarget, arguments);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ package net.minecraftforge.fml.loading;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import net.minecraftforge.forgespi.language.IModInfo.ModVersion;
|
||||
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
|
||||
@@ -20,229 +20,250 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.LOADING;
|
||||
|
||||
public class ModSorter
|
||||
{
|
||||
public final class ModSorter {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final UniqueModListBuilder uniqueModListBuilder;
|
||||
private List<ModFile> modFiles;
|
||||
private List<ModInfo> sortedList;
|
||||
private Map<String, IModInfo> modIdNameLookup;
|
||||
private List<ModFile> systemMods;
|
||||
|
||||
private ModSorter(final List<ModFile> modFiles)
|
||||
{
|
||||
this.uniqueModListBuilder = new UniqueModListBuilder(modFiles);
|
||||
}
|
||||
record State(List<ModFile> files, List<ModInfo> mods) {}
|
||||
|
||||
private ModSorter() {}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static LoadingModList sort(List<ModFile> mods, final List<ExceptionData> errors) {
|
||||
State systemMods = detectSystemMods(mods);
|
||||
List<ModFile> modFiles;
|
||||
|
||||
public static LoadingModList sort(List<ModFile> mods, final List<ExceptionData> errors)
|
||||
{
|
||||
final ModSorter ms = new ModSorter(mods);
|
||||
try {
|
||||
ms.buildUniqueList();
|
||||
modFiles = UniqueModListBuilder.buildUniqueList(mods).modFiles();
|
||||
} catch (EarlyLoadingException e) {
|
||||
// We cannot build any list with duped mods. We have to abort immediately and report it
|
||||
// Note this will never actually throw an error because the duplicate checks are done in ModDiscovererer before we get to this phase
|
||||
// So all this is really doing is wasting time.
|
||||
// But i'm leaving it here until I rewrite all of cpw's mod loading code because its such a clusterfuck.
|
||||
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).collect(toList()), e);
|
||||
return LoadingModListImpl.init(systemMods, e);
|
||||
}
|
||||
|
||||
var named = new HashMap<String, ModInfo>();
|
||||
for (var file : modFiles) {
|
||||
for (var info : file.getModInfos())
|
||||
named.put(info.getModId(), (ModInfo)info);
|
||||
}
|
||||
|
||||
// try and validate dependencies
|
||||
final List<ExceptionData> failedList = Stream.concat(ms.verifyDependencyVersions().stream(), errors.stream()).toList();
|
||||
final List<ExceptionData> failedList = Stream.concat(verifyDependencyVersions(modFiles).stream(), errors.stream()).toList();
|
||||
|
||||
// if we miss one or the other, we abort now
|
||||
if (!failedList.isEmpty()) {
|
||||
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).collect(toList()), new EarlyLoadingException("failure to validate mod list", null, failedList));
|
||||
return LoadingModListImpl.init(systemMods, new EarlyLoadingException("failure to validate mod list", null, failedList));
|
||||
} else {
|
||||
// Otherwise, lets try and sort the modlist and proceed
|
||||
EarlyLoadingException earlyLoadingException = null;
|
||||
try {
|
||||
ms.sort();
|
||||
var sorted = sort(modFiles, named);
|
||||
return LoadingModListImpl.init(sorted, null);
|
||||
} catch (EarlyLoadingException e) {
|
||||
earlyLoadingException = e;
|
||||
// The only exception that can happen here is a cyclic exception, but fall back to system mods so we can display the nice screen.
|
||||
return LoadingModListImpl.init(systemMods, e);
|
||||
}
|
||||
return LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException);
|
||||
}
|
||||
}
|
||||
|
||||
private void sort()
|
||||
{
|
||||
// lambdas are identity based, so sorting them is impossible unless you hold reference to them
|
||||
final MutableGraph<ModFileInfo> graph = GraphBuilder.directed().build();
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
Map<ModFileInfo, Integer> infos = modFiles.stream()
|
||||
.map(ModFile::getModFileInfo)
|
||||
.filter(ModFileInfo.class::isInstance)
|
||||
.map(ModFileInfo.class::cast)
|
||||
.collect(toMap(Function.identity(), e -> counter.incrementAndGet()));
|
||||
infos.keySet().forEach(graph::addNode);
|
||||
modFiles.stream()
|
||||
.map(ModFile::getModInfos)
|
||||
.<IModInfo>mapMulti(Iterable::forEach)
|
||||
.map(IModInfo::getDependencies)
|
||||
.<IModInfo.ModVersion>mapMulti(Iterable::forEach)
|
||||
.forEach(dep -> addDependency(graph, dep));
|
||||
private static State sort(final List<ModFile> modFiles, final Map<String, ModInfo> named) {
|
||||
final MutableGraph<ModInfo> graph = GraphBuilder.directed().build();
|
||||
|
||||
final List<ModFileInfo> sorted;
|
||||
try
|
||||
{
|
||||
sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get));
|
||||
}
|
||||
catch (CyclePresentException e)
|
||||
{
|
||||
Set<Set<ModFileInfo>> cycles = e.getCycles();
|
||||
if (LOGGER.isErrorEnabled(LOADING))
|
||||
{
|
||||
LOGGER.error(LOADING, "Mod Sorting failed.\nDetected Cycles: {}\n", cycles);
|
||||
int counter = 0;
|
||||
var infos = new HashMap<ModInfo, Integer>();
|
||||
for (var file : modFiles) {
|
||||
if (file.getModFileInfo() instanceof ModFileInfo info) {
|
||||
for (var imod : info.getMods()) {
|
||||
var mod = (ModInfo)imod;
|
||||
infos.put(mod, counter++);
|
||||
graph.addNode(mod);
|
||||
}
|
||||
}
|
||||
var dataList = cycles.stream()
|
||||
.<ModFileInfo>mapMulti(Iterable::forEach)
|
||||
.<IModInfo>mapMulti((mf,c)->mf.getMods().forEach(c))
|
||||
.map(IModInfo::getModId)
|
||||
.map(list -> new ExceptionData("fml.modloading.cycle", list))
|
||||
.toList();
|
||||
}
|
||||
|
||||
for (var file : modFiles) {
|
||||
for (var info : file.getModInfos()) {
|
||||
for (var dep : info.getDependencies()) {
|
||||
// Ordering isn't effected by sides, should it be?
|
||||
//if (!dep.getSide().isCorrectSide())
|
||||
// continue;
|
||||
|
||||
var target = named.get(dep.getModId());
|
||||
|
||||
// soft dep that doesn't exist. No edge required.
|
||||
if (target == null)
|
||||
continue;
|
||||
|
||||
var self = (ModInfo)dep.getOwner();
|
||||
|
||||
switch (dep.getOrdering()) {
|
||||
case BEFORE -> graph.putEdge(self, target);
|
||||
case AFTER -> graph.putEdge(target, self);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final List<ModInfo> sorted;
|
||||
try {
|
||||
sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get));
|
||||
} catch (CyclePresentException e) {
|
||||
Set<Set<ModInfo>> cycles = e.getCycles();
|
||||
var buf = new StringBuilder();
|
||||
buf.append("Mod Sorting failed - Detected Cycles: \n");
|
||||
|
||||
var dataList = new ArrayList<ExceptionData>();
|
||||
for (var cycle : cycles) {
|
||||
buf.append("\tCycle:\n");
|
||||
for (var mod : cycle) {
|
||||
var modDeps = new StringBuilder()
|
||||
.append(mod.getModId())
|
||||
.append(' ')
|
||||
.append(mod.getDependencies().stream()
|
||||
.filter(v -> cycle.stream().anyMatch(m -> m.getModId().equals(v.getModId())))
|
||||
.map(dep -> dep.getOrdering().name() + " " + dep.getModId())
|
||||
.collect(Collectors.joining(", "))
|
||||
);
|
||||
dataList.add(new ExceptionData("fml.modloading.cycle", modDeps.toString()));
|
||||
buf.append("\t\tMod: ").append(modDeps.toString()).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error(LOADING, buf.toString());
|
||||
|
||||
throw new EarlyLoadingException("Sorting error", e, dataList);
|
||||
}
|
||||
this.sortedList = sorted.stream()
|
||||
.map(ModFileInfo::getMods)
|
||||
.<IModInfo>mapMulti(Iterable::forEach)
|
||||
.map(ModInfo.class::cast)
|
||||
.collect(toList());
|
||||
this.modFiles = sorted.stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
private void addDependency(MutableGraph<ModFileInfo> topoGraph, IModInfo.ModVersion dep)
|
||||
{
|
||||
final ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile();
|
||||
final IModInfo targetModInfo = modIdNameLookup.get(dep.getModId());
|
||||
// soft dep that doesn't exist. Just return. No edge required.
|
||||
if (targetModInfo == null || !(targetModInfo.getOwningFile() instanceof final ModFileInfo target)) return;
|
||||
if (self == target)
|
||||
return; // in case a jar has two mods that have dependencies between
|
||||
switch (dep.getOrdering()) {
|
||||
case BEFORE -> topoGraph.putEdge(self, target);
|
||||
case AFTER -> topoGraph.putEdge(target, self);
|
||||
default -> {}
|
||||
var files = new LinkedHashSet<ModFile>();
|
||||
var list = new ArrayList<ModInfo>();
|
||||
|
||||
for (var mod : sorted) {
|
||||
files.add(mod.getOwningFile().getFile());
|
||||
list.add(mod);
|
||||
}
|
||||
|
||||
return new State(files.stream().toList(), list);
|
||||
}
|
||||
|
||||
private void buildUniqueList()
|
||||
{
|
||||
final UniqueModListBuilder.UniqueModListData uniqueModListData = uniqueModListBuilder.buildUniqueList();
|
||||
|
||||
this.modFiles = uniqueModListData.modFiles();
|
||||
|
||||
detectSystemMods(uniqueModListData.modFilesByFirstId());
|
||||
|
||||
modIdNameLookup = uniqueModListData.modFilesByFirstId().entrySet().stream()
|
||||
.filter(e -> !e.getValue().get(0).getModInfos().isEmpty())
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> e.getValue().get(0).getModInfos().get(0)
|
||||
));
|
||||
}
|
||||
|
||||
private void detectSystemMods(final Map<String, List<ModFile>> modFilesByFirstId) {
|
||||
private static State detectSystemMods(final List<ModFile> modFiles) {
|
||||
// Capture system mods (ex. MC, Forge) here, so we can keep them for later
|
||||
var systemMods = List.of("minecraft", "forge");
|
||||
LOGGER.debug("Configured system mods: {}", systemMods);
|
||||
|
||||
this.systemMods = new ArrayList<>();
|
||||
var mods = new ArrayList<ModInfo>();
|
||||
var files = new ArrayList<ModFile>();
|
||||
for (var systemMod : systemMods) {
|
||||
var container = modFilesByFirstId.get(systemMod);
|
||||
if (container != null && !container.isEmpty()) {
|
||||
LOGGER.debug("Found system mod: {}", systemMod);
|
||||
this.systemMods.add((ModFile) container.get(0));
|
||||
} else {
|
||||
var mod = findMod(modFiles, systemMod);
|
||||
if (mod == null)
|
||||
throw new IllegalStateException("Failed to find system mod: " + systemMod);
|
||||
}
|
||||
|
||||
LOGGER.debug("Found system mod: {}", systemMod);
|
||||
mods.add(mod.info());
|
||||
files.add(mod.file());
|
||||
}
|
||||
|
||||
return new State(files, mods);
|
||||
}
|
||||
|
||||
private List<EarlyLoadingException.ExceptionData> verifyDependencyVersions()
|
||||
{
|
||||
final var modVersions = modFiles.stream()
|
||||
.map(ModFile::getModInfos)
|
||||
.<IModInfo>mapMulti(Iterable::forEach)
|
||||
.collect(toMap(IModInfo::getModId, IModInfo::getVersion));
|
||||
private record ModPair(ModFile file, ModInfo info) {}
|
||||
private static ModPair findMod(final List<ModFile> modFiles, String name) {
|
||||
for (var file : modFiles) {
|
||||
for (var mod : file.getModFileInfo().getMods()) {
|
||||
if (name.equals(mod.getModId()))
|
||||
return new ModPair(file, (ModInfo)mod);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final var modVersionDependencies = modFiles.stream()
|
||||
.map(ModFile::getModInfos)
|
||||
.<IModInfo>mapMulti(Iterable::forEach)
|
||||
.collect(groupingBy(Function.identity(), flatMapping(e -> e.getDependencies().stream(), toList())));
|
||||
private static List<ExceptionData> verifyDependencyVersions(final List<ModFile> files) {
|
||||
final var modVersions = new HashMap<String, ArtifactVersion>();
|
||||
final var modRequirements = new HashSet<ModVersion>();
|
||||
int mandatoryRequired = 0;
|
||||
|
||||
final var modRequirements = modVersionDependencies.values().stream()
|
||||
.<IModInfo.ModVersion>mapMulti(Iterable::forEach)
|
||||
.filter(mv -> mv.getSide().isCorrectSide())
|
||||
.collect(toSet());
|
||||
for (var file : files) {
|
||||
for (var info : file.getModInfos()) {
|
||||
modVersions.put(info.getModId(), info.getVersion());
|
||||
for (var dep : info.getDependencies()) {
|
||||
if (dep.getSide().isCorrectSide()) {
|
||||
if (modRequirements.add(dep) && dep.isMandatory())
|
||||
mandatoryRequired++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final long mandatoryRequired = modRequirements.stream().filter(IModInfo.ModVersion::isMandatory).count();
|
||||
LOGGER.debug(LOADING, "Found {} mod requirements ({} mandatory, {} optional)", modRequirements.size(), mandatoryRequired, modRequirements.size() - mandatoryRequired);
|
||||
final var missingVersions = modRequirements.stream()
|
||||
.filter(mv -> (mv.isMandatory() || modVersions.containsKey(mv.getModId())) && this.modVersionNotContained(mv, modVersions))
|
||||
.collect(toSet());
|
||||
final long mandatoryMissing = missingVersions.stream().filter(IModInfo.ModVersion::isMandatory).count();
|
||||
LOGGER.debug(LOADING, "Found {} mod requirements missing ({} mandatory, {} optional)", missingVersions.size(), mandatoryMissing, missingVersions.size() - mandatoryMissing);
|
||||
|
||||
if (!missingVersions.isEmpty()) {
|
||||
if (mandatoryMissing > 0) {
|
||||
LOGGER.error(
|
||||
LOADING,
|
||||
"Missing or unsupported mandatory dependencies:\n{}",
|
||||
missingVersions.stream()
|
||||
.filter(IModInfo.ModVersion::isMandatory)
|
||||
.map(ver -> formatDependencyError(ver, modVersions))
|
||||
.collect(Collectors.joining("\n"))
|
||||
);
|
||||
}
|
||||
if (missingVersions.size() - mandatoryMissing > 0) {
|
||||
LOGGER.error(
|
||||
LOADING,
|
||||
"Unsupported installed optional dependencies:\n{}",
|
||||
missingVersions.stream()
|
||||
.filter(ver -> !ver.isMandatory())
|
||||
.map(ver -> formatDependencyError(ver, modVersions))
|
||||
.collect(Collectors.joining("\n"))
|
||||
);
|
||||
}
|
||||
final var missingMandatory = new HashSet<ModVersion>();
|
||||
final var missingOptional = new HashSet<ModVersion>();
|
||||
|
||||
return missingVersions.stream()
|
||||
.map(mv -> new ExceptionData(mv.isMandatory() ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional",
|
||||
mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(),
|
||||
modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null"))))
|
||||
.toList();
|
||||
for (var dep : modRequirements) {
|
||||
var modId = dep.getModId();
|
||||
var existing = modVersions.get(modId);
|
||||
|
||||
if (!dep.isMandatory() && existing == null)
|
||||
continue;
|
||||
|
||||
var range = dep.getVersionRange();
|
||||
if (existing != null && (range.containsVersion(existing) || "0.0NONE".equals(existing.toString())))
|
||||
continue;
|
||||
|
||||
if (!VersionSupportMatrix.testVersionSupportMatrix(range, modId, "mod"))
|
||||
(dep.isMandatory() ? missingMandatory : missingOptional).add(dep);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
|
||||
|
||||
LOGGER.debug(LOADING, "Found {} mod requirements missing ({} mandatory, {} optional)", missingMandatory.size() + missingOptional.size(), missingMandatory.size(), missingOptional.size());
|
||||
|
||||
var ret = new ArrayList<ExceptionData>();
|
||||
|
||||
if (!missingMandatory.isEmpty()) {
|
||||
LOGGER.error(LOADING, "Missing or unsupported mandatory dependencies:\n{}", formatDependencyError(missingMandatory, modVersions));
|
||||
for (var mv : missingMandatory)
|
||||
ret.add(data(mv, modVersions, "fml.modloading.missingdependency"));
|
||||
}
|
||||
|
||||
if (!missingOptional.isEmpty()) {
|
||||
LOGGER.error(LOADING, "Unsupported installed optional dependencies:\n{}", formatDependencyError(missingMandatory, modVersions));
|
||||
for (var mv : missingMandatory)
|
||||
ret.add(data(mv, modVersions, "fml.modloading.missingdependency.optional"));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static String formatDependencyError(IModInfo.ModVersion dependency, Map<String, ArtifactVersion> modVersions)
|
||||
{
|
||||
ArtifactVersion installed = modVersions.get(dependency.getModId());
|
||||
return String.format(
|
||||
private static String formatDependencyError(Collection<ModVersion> missing, Map<String, ArtifactVersion> modVersions) {
|
||||
var ret = new ArrayList<String>();
|
||||
for (var dep : missing) {
|
||||
var installed = modVersions.get(dep.getModId());
|
||||
ret.add(String.format(
|
||||
"\tMod ID: '%s', Requested by: '%s', Expected range: '%s', Actual version: '%s'",
|
||||
dependency.getModId(),
|
||||
dependency.getOwner().getModId(),
|
||||
dependency.getVersionRange(),
|
||||
dep.getModId(),
|
||||
dep.getOwner().getModId(),
|
||||
dep.getVersionRange(),
|
||||
installed != null ? installed.toString() : "[MISSING]"
|
||||
);
|
||||
));
|
||||
}
|
||||
return String.join("\n", ret);
|
||||
}
|
||||
|
||||
private boolean modVersionNotContained(final IModInfo.ModVersion mv, final Map<String, ArtifactVersion> modVersions)
|
||||
{
|
||||
return !(VersionSupportMatrix.testVersionSupportMatrix(mv.getVersionRange(), mv.getModId(), "mod", (modId, range) -> modVersions.containsKey(modId) &&
|
||||
(range.containsVersion(modVersions.get(modId)) || modVersions.get(modId).toString().equals("0.0NONE"))));
|
||||
private static final ArtifactVersion NULL_VERSION = new DefaultArtifactVersion("null");
|
||||
private static ExceptionData data(ModVersion mv, Map<String, ArtifactVersion> modVersions, String key) {
|
||||
return new ExceptionData(key, mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), modVersions.getOrDefault(mv.getModId(), NULL_VERSION));
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,8 @@ package net.minecraftforge.fml.loading;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.common.collect.Streams;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
@@ -28,205 +22,23 @@ import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
public class RuntimeDistCleaner implements ILaunchPluginService
|
||||
{
|
||||
public final class RuntimeDistCleaner implements ILaunchPluginService {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final Marker DISTXFORM = MarkerFactory.getMarker("DISTXFORM");
|
||||
private static String DIST;
|
||||
private static final String ONLYIN = Type.getDescriptor(OnlyIn.class);
|
||||
private static final String ONLYINS = Type.getDescriptor(OnlyIns.class);
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
public String name() {
|
||||
return "runtimedistcleaner";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason)
|
||||
{
|
||||
AtomicBoolean changes = new AtomicBoolean();
|
||||
if (remove(classNode.visibleAnnotations, DIST))
|
||||
{
|
||||
LOGGER.error(DISTXFORM, "Attempted to load class {} for invalid dist {}", classNode.name, DIST);
|
||||
throw new RuntimeException("Attempted to load class "+ classNode.name + " for invalid dist "+ DIST);
|
||||
}
|
||||
|
||||
if (!FMLEnvironment.production && hasOnlyInWithModAnnotation(classNode.visibleAnnotations))
|
||||
{
|
||||
LOGGER.error(DISTXFORM, "Attempted to load class {} with @Mod and @OnlyIn/@OnlyIns annotations", classNode.name);
|
||||
throw new RuntimeException("Found @OnlyIn on @Mod class "+ classNode.name + " - this is not allowed as it causes crashes. Remove the OnlyIn and consider setting clientSideOnly=true in the root of your mods.toml instead");
|
||||
}
|
||||
|
||||
if (classNode.interfaces != null )
|
||||
{
|
||||
unpack(classNode.visibleAnnotations).stream()
|
||||
.filter(ann->Objects.equals(ann.desc, ONLYIN))
|
||||
.filter(ann-> ann.values.contains("_interface"))
|
||||
.filter(ann->!Objects.equals(((String[])ann.values.get(ann.values.indexOf("value") + 1))[1], DIST))
|
||||
.map(ann -> ((Type)ann.values.get(ann.values.indexOf("_interface") + 1)).getInternalName())
|
||||
.forEach(intf -> {
|
||||
if (classNode.interfaces.remove(intf)) {
|
||||
LOGGER.debug(DISTXFORM,"Removing Interface: {} implements {}", classNode.name, intf);
|
||||
changes.compareAndSet(false, true);
|
||||
}
|
||||
});
|
||||
|
||||
//Remove Class level @OnlyIn/@OnlyIns annotations, this is important if anyone gets ambitious and tries to reflect an annotation with _interface set.
|
||||
if (classNode.visibleAnnotations != null) {
|
||||
Iterator<AnnotationNode> itr = classNode.visibleAnnotations.iterator();
|
||||
while (itr.hasNext()) {
|
||||
AnnotationNode ann = itr.next();
|
||||
if (Objects.equals(ann.desc, ONLYIN) || Objects.equals(ann.desc, ONLYINS)) {
|
||||
LOGGER.debug(DISTXFORM,"Removing Class Annotation: {} @{}", classNode.name, ann.desc);
|
||||
itr.remove();
|
||||
changes.compareAndSet(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<FieldNode> fields = classNode.fields.iterator();
|
||||
while(fields.hasNext())
|
||||
{
|
||||
FieldNode field = fields.next();
|
||||
if (remove(field.visibleAnnotations, DIST))
|
||||
{
|
||||
LOGGER.debug(DISTXFORM,"Removing field: {}.{}", classNode.name, field.name);
|
||||
fields.remove();
|
||||
changes.compareAndSet(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
LambdaGatherer lambdaGatherer = new LambdaGatherer();
|
||||
Iterator<MethodNode> methods = classNode.methods.iterator();
|
||||
while(methods.hasNext())
|
||||
{
|
||||
MethodNode method = methods.next();
|
||||
if (remove(method.visibleAnnotations, DIST))
|
||||
{
|
||||
LOGGER.debug(DISTXFORM,"Removing method: {}.{}{}", classNode.name, method.name, method.desc);
|
||||
methods.remove();
|
||||
lambdaGatherer.accept(method);
|
||||
changes.compareAndSet(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// remove dynamic synthetic lambda methods that are inside of removed methods
|
||||
for (List<Handle> dynamicLambdaHandles = lambdaGatherer.getDynamicLambdaHandles();
|
||||
!dynamicLambdaHandles.isEmpty(); dynamicLambdaHandles = lambdaGatherer.getDynamicLambdaHandles())
|
||||
{
|
||||
lambdaGatherer = new LambdaGatherer();
|
||||
methods = classNode.methods.iterator();
|
||||
while (methods.hasNext())
|
||||
{
|
||||
MethodNode method = methods.next();
|
||||
if ((method.access & Opcodes.ACC_SYNTHETIC) == 0) continue;
|
||||
for (Handle dynamicLambdaHandle : dynamicLambdaHandles)
|
||||
{
|
||||
if (method.name.equals(dynamicLambdaHandle.getName()) && method.desc.equals(dynamicLambdaHandle.getDesc()))
|
||||
{
|
||||
LOGGER.debug(DISTXFORM,"Removing lambda method: {}.{}{}", classNode.name, method.name, method.desc);
|
||||
methods.remove();
|
||||
lambdaGatherer.accept(method);
|
||||
changes.compareAndSet(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changes.get() ? ComputeFlags.SIMPLE_REWRITE : ComputeFlags.NO_REWRITE;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<AnnotationNode> unpack(final List<AnnotationNode> anns)
|
||||
{
|
||||
if (anns == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AnnotationNode> unpacked = new ArrayList<>();
|
||||
|
||||
for (var annotationNode : anns)
|
||||
{
|
||||
if (Objects.equals(annotationNode.desc, ONLYINS))
|
||||
{
|
||||
unpacked.add(annotationNode);
|
||||
|
||||
if (annotationNode.values != null)
|
||||
{
|
||||
List<AnnotationNode> subNodes = (List<AnnotationNode>) annotationNode.values.get(annotationNode.values.indexOf("value") + 1);
|
||||
|
||||
if (subNodes != null)
|
||||
{
|
||||
unpacked.addAll(subNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unpacked;
|
||||
}
|
||||
|
||||
private static boolean remove(final List<AnnotationNode> anns, final String side)
|
||||
{
|
||||
var onlyIns = unpack(anns);
|
||||
|
||||
for (var onlyIn : onlyIns)
|
||||
{
|
||||
if (!Objects.equals(onlyIn.desc, ONLYINS) || onlyIn.values.contains("_interface"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Objects.equals(((String[])onlyIn.values.get(onlyIn.values.indexOf("value") + 1))[1], side))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean hasOnlyInWithModAnnotation(final List<AnnotationNode> anns)
|
||||
{
|
||||
if (anns == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var foundModAnnotation = false;
|
||||
var foundOnlyIn = false;
|
||||
|
||||
for (var ann : anns)
|
||||
{
|
||||
if (ann.desc.equals("Lnet/minecraftforge/fml/common/Mod;"))
|
||||
{
|
||||
foundModAnnotation = true;
|
||||
} else if (Objects.equals(ann.desc, ONLYIN) || Objects.equals(ann.desc, ONLYINS))
|
||||
{
|
||||
foundOnlyIn = true;
|
||||
}
|
||||
}
|
||||
|
||||
return foundModAnnotation && foundOnlyIn;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Consumer<Dist> getExtension()
|
||||
{
|
||||
return (s)-> {
|
||||
DIST = s.name();
|
||||
LOGGER.debug(DISTXFORM, "Configuring for Dist {}", DIST);
|
||||
};
|
||||
}
|
||||
|
||||
private static final EnumSet<Phase> YAY = EnumSet.of(Phase.AFTER);
|
||||
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
|
||||
|
||||
@@ -236,13 +48,179 @@ public class RuntimeDistCleaner implements ILaunchPluginService
|
||||
return NAY;
|
||||
|
||||
String internalName = classType.getInternalName();
|
||||
|
||||
// Only apply to MC classes when on a dedicated server in a production environment
|
||||
if (FMLEnvironment.production)
|
||||
return FMLEnvironment.dist.isDedicatedServer() && isMinecraftClass(internalName) ? YAY : NAY;
|
||||
|
||||
// For dev time it's a bit trickier so that we can throw exceptions when someone uses @OnlyIn in their mod code,
|
||||
// so that it's more obvious that the behaviour they're expecting won't work in production anymore
|
||||
if (internalName.startsWith("net/minecraftforge/"))
|
||||
return NAY;
|
||||
|
||||
return YAY;
|
||||
// No Vanilla classes use @OnlyIn(Dist.SERVER), so we can skip them entirely when we're running on a client
|
||||
if (FMLEnvironment.dist.isClient() && isMinecraftClass(internalName))
|
||||
return NAY;
|
||||
|
||||
return YAY; // mod classes might have @OnlyIn, so parse them in dev to find out and throw if they do
|
||||
}
|
||||
|
||||
private static class LambdaGatherer extends MethodVisitor {
|
||||
@Override
|
||||
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason) {
|
||||
var changed = false;
|
||||
|
||||
var annotations = unpack(classNode.visibleAnnotations);
|
||||
var isNonMinecraftClass = !isMinecraftClass(classNode.name);
|
||||
|
||||
if (remove(annotations, DIST)) {
|
||||
LOGGER.error(DISTXFORM, "Attempted to load class {} for invalid dist {}", classNode.name, DIST);
|
||||
throw new RuntimeException("Attempted to load class " + classNode.name + " for invalid dist " + DIST);
|
||||
}
|
||||
|
||||
if (isNonMinecraftClass && !annotations.isEmpty())
|
||||
throw new UnsupportedOperationException("Mod class " + classNode.name + " is annotated with @OnlyIn, this is no longer supported as it slowed down startup times");
|
||||
|
||||
if (classNode.interfaces != null && !classNode.interfaces.isEmpty()) {
|
||||
for (var ann : annotations) {
|
||||
if (ann.intf == null || DIST.equals(ann.side))
|
||||
continue;
|
||||
|
||||
if (classNode.interfaces.remove(ann.intf)) {
|
||||
LOGGER.debug(DISTXFORM, "Removing Interface: {} implements {}", classNode.name, ann.intf);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Remove Class level @OnlyIn/@OnlyIns annotations, this is important if anyone gets ambitious and tries to reflect an annotation with _interface set.
|
||||
if (classNode.visibleAnnotations != null) {
|
||||
for (var itr = classNode.visibleAnnotations.iterator(); itr.hasNext(); ) {
|
||||
var ann = itr.next();
|
||||
if (ONLYIN.equals(ann.desc) || ONLYINS.equals(ann.desc)) {
|
||||
LOGGER.debug(DISTXFORM, "Removing Class Annotation: {} @{}", classNode.name, ann.desc);
|
||||
itr.remove();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var itr = classNode.fields.iterator(); itr.hasNext(); ) {
|
||||
var field = itr.next();
|
||||
if (remove(unpack(field.visibleAnnotations), DIST)) {
|
||||
LOGGER.debug(DISTXFORM, "Removing field: {}.{}", classNode.name, field.name);
|
||||
itr.remove();
|
||||
changed = true;
|
||||
|
||||
if (isNonMinecraftClass)
|
||||
throw new UnsupportedOperationException("Field " + field.name + " in mod class " + classNode.name + " is annotated with @OnlyIn, this is no longer supported as it slowed down startup times");
|
||||
}
|
||||
}
|
||||
|
||||
var lambdaGatherer = new LambdaGatherer();
|
||||
for (var itr = classNode.methods.iterator(); itr.hasNext(); ) {
|
||||
var method = itr.next();
|
||||
if (remove(unpack(method.visibleAnnotations), DIST)) {
|
||||
LOGGER.debug(DISTXFORM, "Removing method: {}.{}{}", classNode.name, method.name, method.desc);
|
||||
itr.remove();
|
||||
lambdaGatherer.accept(method);
|
||||
changed = true;
|
||||
|
||||
if (isNonMinecraftClass)
|
||||
throw new UnsupportedOperationException("Method " + method.name + " in mod class " + classNode.name + " is annotated with @OnlyIn, this is no longer supported as it slowed down startup times");
|
||||
}
|
||||
}
|
||||
|
||||
// remove dynamic synthetic lambda methods that are inside of removed methods
|
||||
for (List<Handle> handles = lambdaGatherer.getDynamicLambdaHandles();
|
||||
!handles.isEmpty(); handles = lambdaGatherer.getDynamicLambdaHandles()) {
|
||||
lambdaGatherer = new LambdaGatherer();
|
||||
for (var itr = classNode.methods.iterator(); itr.hasNext(); ) {
|
||||
MethodNode method = itr.next();
|
||||
if ((method.access & Opcodes.ACC_SYNTHETIC) == 0)
|
||||
continue;
|
||||
|
||||
for (var handle : handles) {
|
||||
if (method.name.equals(handle.getName()) && method.desc.equals(handle.getDesc())) {
|
||||
LOGGER.debug(DISTXFORM, "Removing lambda method: {}.{}{}", classNode.name, method.name, method.desc);
|
||||
itr.remove();
|
||||
lambdaGatherer.accept(method);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? ComputeFlags.SIMPLE_REWRITE : ComputeFlags.NO_REWRITE;
|
||||
}
|
||||
|
||||
private record Target(String side, String intf) {
|
||||
static Target from(final AnnotationNode node) {
|
||||
var idx = node.values.indexOf("value");
|
||||
var value = (String[])node.values.get(idx + 1); // Enums are stored as [Type, Value]
|
||||
|
||||
idx = node.values.indexOf("_interface");
|
||||
if (idx == -1)
|
||||
return new Target(value[1], null);
|
||||
|
||||
var intf = (Type)node.values.get(idx + 1);
|
||||
return new Target(value[1], intf.getInternalName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack any OnlyIn or OnlyIns annotations to Target objects.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Target> unpack(final List<AnnotationNode> anns) {
|
||||
if (anns == null || anns.isEmpty())
|
||||
return Collections.emptyList();
|
||||
|
||||
var ret = new ArrayList<Target>();
|
||||
for (var node : anns) {
|
||||
if (ONLYIN.equals(node.desc))
|
||||
ret.add(Target.from(node));
|
||||
else if (ONLYINS.equals(node.desc)) {
|
||||
if (node.values == null || node.values.isEmpty())
|
||||
continue;
|
||||
|
||||
int idx = node.values.indexOf("value");
|
||||
if (idx == -1)
|
||||
continue;
|
||||
|
||||
var subNodes = (List<AnnotationNode>)node.values.get(idx + 1);
|
||||
if (subNodes != null) {
|
||||
for (var sub : subNodes)
|
||||
ret.add(Target.from(sub));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static boolean remove(final List<Target> targets, final String side) {
|
||||
for (var target : targets) {
|
||||
if (target.intf == null && !side.equals(target.side))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isMinecraftClass(final String internalName) {
|
||||
return internalName.startsWith("net/minecraft/") || internalName.startsWith("com/mojang/");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Consumer<Dist> getExtension() {
|
||||
return s -> {
|
||||
DIST = s.name();
|
||||
LOGGER.debug(DISTXFORM, "Configuring for Dist {}", DIST);
|
||||
};
|
||||
}
|
||||
|
||||
private static final class LambdaGatherer extends MethodVisitor {
|
||||
private static final Handle META_FACTORY = new Handle(Opcodes.H_INVOKESTATIC,
|
||||
"java/lang/invoke/LambdaMetafactory", "metafactory",
|
||||
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
|
||||
@@ -254,23 +232,21 @@ public class RuntimeDistCleaner implements ILaunchPluginService
|
||||
}
|
||||
|
||||
public void accept(MethodNode method) {
|
||||
Streams.stream(method.instructions.iterator()).
|
||||
filter(insnNode->insnNode.getType() == AbstractInsnNode.INVOKE_DYNAMIC_INSN).
|
||||
forEach(insnNode->insnNode.accept(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs)
|
||||
{
|
||||
if (META_FACTORY.equals(bsm))
|
||||
{
|
||||
Handle dynamicLambdaHandle = (Handle) bsmArgs[1];
|
||||
dynamicLambdaHandles.add(dynamicLambdaHandle);
|
||||
for (var insn : method.instructions) {
|
||||
if (insn.getType() == AbstractInsnNode.INVOKE_DYNAMIC_INSN)
|
||||
insn.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Handle> getDynamicLambdaHandles()
|
||||
{
|
||||
@Override
|
||||
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
|
||||
if (META_FACTORY.equals(bsm)) {
|
||||
var handle = (Handle)bsmArgs[1];
|
||||
dynamicLambdaHandles.add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Handle> getDynamicLambdaHandles() {
|
||||
return dynamicLambdaHandles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,20 @@ import java.util.Map;
|
||||
import org.apache.commons.lang3.text.StrLookup;
|
||||
import org.apache.commons.lang3.text.StrSubstitutor;
|
||||
|
||||
// TODO: [FML] Remove this
|
||||
@SuppressWarnings("deprecation")
|
||||
public class StringSubstitutor {
|
||||
private static final Map<String, String> GLOBALS = Map.of(
|
||||
"mcVersion", FMLLoader.versionInfo().mcVersion(),
|
||||
"forgeVersion", FMLLoader.versionInfo().forgeVersion()
|
||||
);
|
||||
|
||||
public static String replace(final String in, final ModFile file) {
|
||||
return new StrSubstitutor(getStringLookup(file)).replace(in);
|
||||
}
|
||||
|
||||
private static StrLookup<String> getStringLookup(final ModFile file) {
|
||||
var globals = Map.of(
|
||||
"mcVersion", FMLLoader.versionInfo().mcVersion(),
|
||||
"forgeVersion", FMLLoader.versionInfo().forgeVersion()
|
||||
);
|
||||
return new StrLookup<String>() {
|
||||
return new StrLookup<>() {
|
||||
@Override
|
||||
public String lookup(String key) {
|
||||
var parts = key.split("\\.");
|
||||
@@ -32,7 +34,7 @@ public class StringSubstitutor {
|
||||
var pfx = parts[0];
|
||||
|
||||
if ("global".equals(pfx))
|
||||
return globals.get(parts[1]);
|
||||
return GLOBALS.get(parts[1]);
|
||||
else if ("file".equals(pfx) && file != null)
|
||||
return String.valueOf(file.getSubstitutionMap().get().get(parts[1]));
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public class StringUtils {
|
||||
return java.util.stream.Stream.of(endings).anyMatch(lowerSearch::endsWith);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static URL toURL(final String string) {
|
||||
if (string == null || string.trim().isEmpty() || string.contains("myurl.me") || string.contains("example.invalid"))
|
||||
return null;
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -16,26 +17,19 @@ import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.minecraftforge.fml.loading.LogMarkers.LOADING;
|
||||
|
||||
public class UniqueModListBuilder
|
||||
{
|
||||
private final static Logger LOGGER = LogUtils.getLogger();
|
||||
@ApiStatus.Internal
|
||||
@Deprecated(since = "1.21.3", forRemoval = true) // TODO: [FML][Loading] Convert to package private in 1.22
|
||||
public final class UniqueModListBuilder {
|
||||
private UniqueModListBuilder() {}
|
||||
|
||||
private final List<ModFile> modFiles;
|
||||
|
||||
public UniqueModListBuilder(final List<ModFile> modFiles) {this.modFiles = modFiles;}
|
||||
|
||||
public UniqueModListData buildUniqueList()
|
||||
{
|
||||
List<ModFile> uniqueModList;
|
||||
List<ModFile> uniqueLibListWithVersion;
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
public static UniqueModListData buildUniqueList(List<ModFile> modFiles) {
|
||||
// Collect mod files by module name. This will be used for deduping purposes
|
||||
final Map<String, List<ModFile>> modFilesByFirstId = modFiles.stream()
|
||||
.filter(mf -> mf.getModFileInfo() != null)
|
||||
@@ -46,13 +40,13 @@ public class UniqueModListBuilder
|
||||
.collect(groupingBy(UniqueModListBuilder::getModId));
|
||||
|
||||
// Select the newest by artifact version sorting of non-unique files thus identified
|
||||
uniqueModList = modFilesByFirstId.entrySet().stream()
|
||||
.map(this::selectNewestModInfo)
|
||||
final List<ModFile> uniqueModList = modFilesByFirstId.entrySet().stream()
|
||||
.map(UniqueModListBuilder::selectNewestModInfo)
|
||||
.toList();
|
||||
|
||||
// Select the newest by artifact version sorting of non-unique files thus identified
|
||||
uniqueLibListWithVersion = libFilesWithVersionByModuleName.entrySet().stream()
|
||||
.map(this::selectNewestModInfo)
|
||||
final List<ModFile> uniqueLibListWithVersion = libFilesWithVersionByModuleName.entrySet().stream()
|
||||
.map(UniqueModListBuilder::selectNewestModInfo)
|
||||
.toList();
|
||||
|
||||
// Transform to the full mod id list
|
||||
@@ -63,43 +57,40 @@ public class UniqueModListBuilder
|
||||
.collect(groupingBy(IModInfo::getModId));
|
||||
|
||||
// Transform to the full lib id list
|
||||
final Map<String, List<ModFile>> versionedLibIds = uniqueLibListWithVersion.stream()
|
||||
final List<List<ModFile>> versionedLibs = uniqueLibListWithVersion.stream()
|
||||
.map(UniqueModListBuilder::getModId)
|
||||
.collect(Collectors.toMap(
|
||||
Function.identity(),
|
||||
libFilesWithVersionByModuleName::get
|
||||
));
|
||||
.map(libFilesWithVersionByModuleName::get)
|
||||
.toList();
|
||||
|
||||
// Its theoretically possible that some mod has somehow moved an id to a secondary place, thus causing a dupe.
|
||||
// We can't handle this
|
||||
final List<String> dupedModErrors = modIds.values().stream()
|
||||
.filter(modInfos -> modInfos.size() > 1)
|
||||
.map(mods -> String.format("\tMod ID: '%s' from mod files: %s",
|
||||
mods.get(0).getModId(),
|
||||
mods.getFirst().getModId(),
|
||||
mods.stream()
|
||||
.map(modInfo -> modInfo.getOwningFile().getFile().getFileName()).collect(joining(", "))
|
||||
)).toList();
|
||||
|
||||
if (!dupedModErrors.isEmpty()) {
|
||||
LOGGER.error(LOADING, "Found duplicate mods:\n{}", dupedModErrors.stream().collect(joining("\n")));
|
||||
LOGGER.error(LOADING, "Found duplicate mods:\n{}", String.join("\n", dupedModErrors));
|
||||
throw new EarlyLoadingException("Duplicate mods found", null, dupedModErrors.stream()
|
||||
.map(s -> new EarlyLoadingException.ExceptionData(s))
|
||||
.map(EarlyLoadingException.ExceptionData::new)
|
||||
.toList());
|
||||
}
|
||||
|
||||
|
||||
final List<String> dupedLibErrors = versionedLibIds.values().stream()
|
||||
.filter(modFiles -> modFiles.size() > 1)
|
||||
final List<String> dupedLibErrors = versionedLibs.stream()
|
||||
.filter(libModFiles -> libModFiles.size() > 1)
|
||||
.map(mods -> String.format("\tLibrary: '%s' from files: %s",
|
||||
getModId(mods.get(0)),
|
||||
mods.stream()
|
||||
.map(modFile -> modFile.getFileName()).collect(joining(", "))
|
||||
getModId(mods.getFirst()),
|
||||
mods.stream().map(ModFile::getFileName).collect(joining(", "))
|
||||
)).toList();
|
||||
|
||||
if (!dupedLibErrors.isEmpty()) {
|
||||
LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", dupedLibErrors.stream().collect(joining("\n")));
|
||||
LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", String.join("\n", dupedLibErrors));
|
||||
throw new EarlyLoadingException("Duplicate plugins or libraries found", null, dupedLibErrors.stream()
|
||||
.map(s -> new EarlyLoadingException.ExceptionData(s))
|
||||
.map(EarlyLoadingException.ExceptionData::new)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@@ -107,40 +98,40 @@ public class UniqueModListBuilder
|
||||
final Map<String, List<ModFile>> uniqueModFilesByFirstId = uniqueModList.stream()
|
||||
.collect(groupingBy(UniqueModListBuilder::getModId));
|
||||
|
||||
final List<ModFile> loadedList = new ArrayList<>();
|
||||
loadedList.addAll(uniqueModList);
|
||||
final List<ModFile> loadedList = new ArrayList<>(uniqueModList);
|
||||
loadedList.addAll(uniqueLibListWithVersion);
|
||||
|
||||
return new UniqueModListData(loadedList, uniqueModFilesByFirstId);
|
||||
}
|
||||
|
||||
private ModFile selectNewestModInfo(Map.Entry<String, List<ModFile>> fullList) {
|
||||
private static ModFile selectNewestModInfo(Map.Entry<String, List<ModFile>> fullList) {
|
||||
List<ModFile> modInfoList = fullList.getValue();
|
||||
if (modInfoList.size() > 1) {
|
||||
LOGGER.debug("Found {} mods for first modid {}, selecting most recent based on version data", modInfoList.size(), fullList.getKey());
|
||||
modInfoList.sort(Comparator.comparing(this::getVersion).reversed());
|
||||
LOGGER.debug("Selected file {} for modid {} with version {}", modInfoList.get(0).getFileName(), fullList.getKey(), this.getVersion(modInfoList.get(0)));
|
||||
modInfoList.sort(Comparator.comparing(UniqueModListBuilder::getVersion).reversed());
|
||||
LOGGER.debug("Selected file {} for modid {} with version {}", modInfoList.getFirst().getFileName(), fullList.getKey(), getVersion(modInfoList.getFirst()));
|
||||
}
|
||||
return modInfoList.get(0);
|
||||
return modInfoList.getFirst();
|
||||
}
|
||||
|
||||
private ArtifactVersion getVersion(final ModFile mf)
|
||||
{
|
||||
if (mf.getModFileInfo() == null || mf.getModInfos() == null || mf.getModInfos().isEmpty()) {
|
||||
private static ArtifactVersion getVersion(final ModFile mf) {
|
||||
List<IModInfo> modInfos;
|
||||
if (mf.getModFileInfo() == null || (modInfos = mf.getModInfos()) == null || modInfos.isEmpty()) {
|
||||
return mf.getJarVersion();
|
||||
}
|
||||
|
||||
return mf.getModInfos().get(0).getVersion();
|
||||
return modInfos.getFirst().getVersion();
|
||||
}
|
||||
|
||||
private static String getModId(ModFile modFile) {
|
||||
if (modFile.getModFileInfo() == null || modFile.getModFileInfo().getMods().isEmpty()) {
|
||||
var modFileInfo = modFile.getModFileInfo();
|
||||
List<IModInfo> mods;
|
||||
if (modFileInfo == null || (mods = modFileInfo.getMods()).isEmpty()) {
|
||||
return modFile.getSecureJar().name();
|
||||
}
|
||||
|
||||
return modFile.getModFileInfo().moduleName();
|
||||
return mods.getFirst().getModId();
|
||||
}
|
||||
|
||||
public record UniqueModListData(List<ModFile> modFiles, Map<String, List<ModFile>> modFilesByFirstId) {}
|
||||
|
||||
}
|
||||
@@ -5,33 +5,43 @@
|
||||
|
||||
package net.minecraftforge.fml.loading;
|
||||
|
||||
import net.minecraftforge.forgespi.language.MavenVersionAdapter;
|
||||
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||
import org.apache.maven.artifact.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.Map;
|
||||
|
||||
@ApiStatus.Internal // since 1.21.1, will be made non-public in a later MC version
|
||||
public class VersionSupportMatrix {
|
||||
private static final HashMap<String, List<ArtifactVersion>> overrideVersions = new HashMap<>();
|
||||
private static final Map<String, List<ArtifactVersion>> OVERRIDE_VERSIONS;
|
||||
|
||||
static {
|
||||
final ArtifactVersion version = new DefaultArtifactVersion(FMLLoader.versionInfo().mcVersion());
|
||||
if (MavenVersionAdapter.createFromVersionSpec("[1.19.2]").containsVersion(version)) {
|
||||
// 1.19.2 is Compatible with 1.19.1
|
||||
add("languageloader.javafml", "42");
|
||||
add("mod.minecraft", "1.19.1");
|
||||
add("mod.forge", "42.0.9");
|
||||
if ("1.21.1".equals(FMLLoader.versionInfo().mcVersion())) {
|
||||
OVERRIDE_VERSIONS = Map.ofEntries(
|
||||
// 1.21.1 is compatible with 1.21
|
||||
entry("languageloader.javafml", "51"),
|
||||
entry("mod.minecraft", "1.21"),
|
||||
entry("mod.forge", "51.0.33")
|
||||
);
|
||||
} else {
|
||||
OVERRIDE_VERSIONS = Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
private static void add(String key, String value) {
|
||||
overrideVersions.computeIfAbsent(key, k -> new ArrayList<>()).add(new DefaultArtifactVersion(value));
|
||||
|
||||
public static boolean testVersionSupportMatrix(VersionRange declaredRange, String lookupId, String type) {
|
||||
if (OVERRIDE_VERSIONS.isEmpty()) return false;
|
||||
List<ArtifactVersion> custom = OVERRIDE_VERSIONS.get(type + "." + lookupId);
|
||||
return custom != null && custom.stream().anyMatch(declaredRange::containsVersion);
|
||||
}
|
||||
public static <T> boolean testVersionSupportMatrix(VersionRange declaredRange, String lookupId, String type, BiPredicate<String, VersionRange> standardLookup) {
|
||||
if (standardLookup.test(lookupId, declaredRange)) return true;
|
||||
List<ArtifactVersion> custom = overrideVersions.get(type + "." + lookupId);
|
||||
return custom == null ? false : custom.stream().anyMatch(declaredRange::containsVersion);
|
||||
|
||||
private static Map.Entry<String, List<ArtifactVersion>> entry(String typeAndLookupId, String declaredRange) {
|
||||
return Map.entry(typeAndLookupId, List.of(new DefaultArtifactVersion(declaredRange)));
|
||||
}
|
||||
|
||||
private static Map.Entry<String, List<ArtifactVersion>> entry(String typeAndLookupId, List<String> declaredRanges) {
|
||||
return Map.entry(typeAndLookupId, declaredRanges.stream().map(DefaultArtifactVersion::new).map(it -> (ArtifactVersion) it).toList());
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,7 @@ import org.apache.logging.log4j.util.PerformanceSensitive;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A wrapper for {@link HighlightConverter} that auto-disables ANSI when the terminal doesn't support it.
|
||||
@@ -49,10 +47,7 @@ public class ForgeHighlight {
|
||||
LOGGER.warn("Failed to invoke initializeTerminal on TCA", e);
|
||||
}
|
||||
if (!TerminalConsoleAppender.isAnsiSupported() && Arrays.stream(options).noneMatch(s -> s.equals("disableAnsi=true"))) {
|
||||
List<String> optionList = new ArrayList<>();
|
||||
optionList.add(options[0]);
|
||||
optionList.add("disableAnsi=true");
|
||||
options = optionList.toArray(new String[0]);
|
||||
options = new String[] { options[0], "disableAnsi=true" };
|
||||
}
|
||||
return HighlightConverter.newInstance(config, options);
|
||||
}
|
||||
|
||||
+83
-12
@@ -6,6 +6,7 @@
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
|
||||
import cpw.mods.jarhandling.JarMetadata;
|
||||
import cpw.mods.jarhandling.SecureJar;
|
||||
import net.minecraftforge.fml.loading.LogMarkers;
|
||||
@@ -22,19 +23,25 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.module.InvalidModuleDescriptorException;
|
||||
import java.lang.module.ModuleDescriptor;
|
||||
import java.lang.module.ModuleDescriptor.Version;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public abstract class AbstractModProvider implements IModProvider {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
protected static final String MODS_TOML = "META-INF/mods.toml";
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
protected static final String MODS_TOML = "META-INF/mods.toml";
|
||||
protected static final String MODULE_INFO = "module-info.class";
|
||||
|
||||
protected IModLocator.ModFileOrException createMod(Path path) {
|
||||
return createMod(path, false);
|
||||
@@ -42,16 +49,22 @@ public abstract class AbstractModProvider implements IModProvider {
|
||||
|
||||
@Nullable
|
||||
protected IModLocator.ModFileOrException createMod(Path path, boolean ignoreUnknown) {
|
||||
return createMod(path, false, getDefaultJarModType());
|
||||
return createMod(path, ignoreUnknown, getDefaultJarModType());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected IModLocator.ModFileOrException createMod(Path path, boolean ignoreUnknown, String defaultType) {
|
||||
var debug = path.toString();
|
||||
if (debug.equals("/"))
|
||||
debug = path.toUri().toString();
|
||||
|
||||
var mjm = new ModJarMetadata();
|
||||
var sj = SecureJar.from(
|
||||
jar -> jar.moduleDataProvider().findFile(MODS_TOML).isPresent() ? mjm : JarMetadata.from(jar, path),
|
||||
path
|
||||
);
|
||||
SecureJar sj = null;
|
||||
try {
|
||||
sj = SecureJar.from(jar -> loadMetaFromJar(jar, mjm), path);
|
||||
} catch (Throwable t) {
|
||||
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Failed to create secure jar for \"" + debug + "\" - " + t.getMessage()));
|
||||
}
|
||||
|
||||
IModFile mod;
|
||||
var type = sj.moduleDataProvider().getManifest().getMainAttributes().getValue(ModFile.TYPE);
|
||||
@@ -59,32 +72,89 @@ public abstract class AbstractModProvider implements IModProvider {
|
||||
type = defaultType;
|
||||
|
||||
if (sj.moduleDataProvider().findFile(MODS_TOML).isPresent()) {
|
||||
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, path);
|
||||
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, debug);
|
||||
mod = new ModFile(sj, this, ModFileParser::modsTomlParser);
|
||||
// ModJarMetadata is only used when loading mods.toml mods as ModJarMetadata
|
||||
mjm.setModFile(mod);
|
||||
if (mod.getModFileInfo().getFileProperties().containsKey(ModFileInfo.NOT_A_FORGE_MOD_PROP)) {
|
||||
LOGGER.error(LogMarkers.SCAN, "Unable to load file \"{}\" because its mods.toml is requesting an invalid javafml loaderVersion (use \"*\" if you want to allow all versions) and is missing a forge modId dependency declaration (see the sample mods.toml in the MDK).", path);
|
||||
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("File \"%s\" is not a Forge mod and cannot be loaded. Look for a Forge version of this mod or consider alternative mods.".formatted(mod.getFileName())));
|
||||
}
|
||||
} else if (type != null) {
|
||||
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", JarFile.MANIFEST_NAME, type, path);
|
||||
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", JarFile.MANIFEST_NAME, type, debug);
|
||||
mod = new ModFile(sj, this, this::manifestParser, type);
|
||||
} else if (ignoreUnknown) {
|
||||
return null;
|
||||
} else
|
||||
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Invalid mod file found " + path));
|
||||
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Invalid mod file found " + debug));
|
||||
|
||||
mjm.setModFile(mod);
|
||||
return new IModLocator.ModFileOrException(mod, null);
|
||||
}
|
||||
|
||||
protected static JarMetadata loadMetaFromJar(SecureJar jar, ModJarMetadata mjm) {
|
||||
var info = jar.moduleDataProvider().open(MODULE_INFO).orElse(null);
|
||||
if (info != null) {
|
||||
try {
|
||||
var desc = ModuleDescriptor.read(info, jar::getPackages);
|
||||
var all = new HashSet<>(jar.getPackages());
|
||||
all.removeAll(desc.packages());
|
||||
if (!all.isEmpty()) {
|
||||
var missing = all.stream().sorted().collect(Collectors.joining(", "));
|
||||
LOGGER.error("Invalid module-info, missing packages " + missing);
|
||||
throw new ModFileLoadingException("Invalid module-info, missing packages " + missing);
|
||||
} else {
|
||||
return new JarMetadata() {
|
||||
@Override
|
||||
public String name() {
|
||||
return desc.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return desc.version().map(Version::toString).or(desc::rawVersion).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModuleDescriptor descriptor() {
|
||||
return desc;
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (InvalidModuleDescriptorException | IOException e) {
|
||||
LOGGER.error("Failed to parse " + jar.getPrimaryPath() + " module-info", e);
|
||||
throw new ModFileLoadingException("Invalid module-info: " + e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
info.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (jar.moduleDataProvider().findFile(MODS_TOML).isEmpty())
|
||||
return JarMetadata.from(jar, jar.getPrimaryPath());
|
||||
|
||||
return mjm;
|
||||
}
|
||||
|
||||
protected IModFileInfo manifestParser(final IModFile mod) {
|
||||
var mf = mod.getSecureJar().moduleDataProvider().getManifest().getMainAttributes();
|
||||
var license = mf.getValue("LICENSE");
|
||||
var dummy = new IConfigurable() {
|
||||
@Override
|
||||
public <T> Optional<T> getConfigElement(String key) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getConfigElement(String... key) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(String key) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(String... key) {
|
||||
return Collections.emptyList();
|
||||
@@ -140,10 +210,11 @@ public abstract class AbstractModProvider implements IModProvider {
|
||||
}
|
||||
|
||||
private static final class Holder<T> {
|
||||
T value;
|
||||
private T value;
|
||||
}
|
||||
|
||||
private record DefaultModFileInfo(IModFile mod, String license, IConfigurable configurable) implements IModFileInfo, IConfigurable {
|
||||
@Override public <T> Optional<T> getConfigElement(final String string) { return Optional.empty(); }
|
||||
@Override public <T> Optional<T> getConfigElement(final String... strings) { return Optional.empty(); }
|
||||
@Override public List<? extends IConfigurable> getConfigList(final String... strings) { return null; }
|
||||
@Override public List<IModInfo> getMods() { return Collections.emptyList(); }
|
||||
|
||||
+7
-19
@@ -7,7 +7,6 @@ package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import net.minecraftforge.fml.loading.LogMarkers;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
import org.slf4j.Logger;
|
||||
@@ -17,8 +16,7 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class BackgroundScanHandler
|
||||
{
|
||||
public final class BackgroundScanHandler {
|
||||
private enum ScanStatus {
|
||||
NOT_STARTED,
|
||||
RUNNING,
|
||||
@@ -29,10 +27,10 @@ public class BackgroundScanHandler
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final boolean DEBUG = LOGGER.isErrorEnabled(LogMarkers.SCAN);
|
||||
private final ExecutorService modContentScanner;
|
||||
private final List<ModFile> modFiles;
|
||||
private ScanStatus status;
|
||||
private LoadingModList loadingModList;
|
||||
|
||||
public BackgroundScanHandler(final List<ModFile> modFiles) {
|
||||
this.modFiles = modFiles;
|
||||
@@ -55,29 +53,19 @@ public class BackgroundScanHandler
|
||||
}
|
||||
status = ScanStatus.RUNNING;
|
||||
ImmediateWindowHandler.updateProgress("Scanning mod candidates");
|
||||
final CompletableFuture<ModFileScanData> future = CompletableFuture.supplyAsync(file::compileContent, modContentScanner)
|
||||
.whenComplete(file::setScanResult)
|
||||
.whenComplete((r,t)-> this.addCompletedFile(file,r,t));
|
||||
CompletableFuture<ModFileScanData> future = CompletableFuture.supplyAsync(file::compileContent, modContentScanner)
|
||||
.whenComplete(file::setScanResult);
|
||||
if (DEBUG) future = future.whenComplete((_, t) -> addCompletedFile(file, t));
|
||||
file.setFutureScanResult(future);
|
||||
}
|
||||
|
||||
private void addCompletedFile(final ModFile file, final ModFileScanData modFileScanData, final Throwable throwable) {
|
||||
private void addCompletedFile(final ModFile file, final Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
status = ScanStatus.ERRORED;
|
||||
LOGGER.error(LogMarkers.SCAN,"An error occurred scanning file {}", file, throwable);
|
||||
LOGGER.error(LogMarkers.SCAN, "An error occurred scanning file {}", file, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoadingModList(LoadingModList loadingModList)
|
||||
{
|
||||
this.loadingModList = loadingModList;
|
||||
}
|
||||
|
||||
public LoadingModList getLoadingModList()
|
||||
{
|
||||
return loadingModList;
|
||||
}
|
||||
|
||||
public void waitForScanToComplete(final Runnable ticker) {
|
||||
boolean timeoutActive = System.getProperty("fml.disableScanTimeout") == null;
|
||||
Instant deadline = Instant.now().plus(Duration.ofMinutes(10));
|
||||
|
||||
+9
-9
@@ -18,9 +18,9 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.jar.Attributes;
|
||||
@@ -28,7 +28,7 @@ import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class ClasspathLocator extends AbstractModProvider implements IModLocator {
|
||||
public final class ClasspathLocator extends AbstractModProvider implements IModLocator {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final Attributes.Name MOD_TYPE = new Attributes.Name("FMLModType");
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ClasspathLocator extends AbstractModProvider implements IModLocator
|
||||
// Find 'minecraft' during dev time this is also Forge. So skip it.
|
||||
// Can return null for dedicated server as Minecraft isn't on the classpath
|
||||
// On the client it doesn't matter as it's the vanilla jar so wouldnt have mods.toml in it.
|
||||
var minecraft = getPathFromResource(cl, "net/minecraft/obfuscate/DontObfuscate.class");
|
||||
var minecraft = getPathFromResource(cl, "net/minecraft/world/item/Item.class");
|
||||
|
||||
var claimed = new HashSet<Path>();
|
||||
var tomls = getUrls(cl, MODS_TOML);
|
||||
@@ -74,17 +74,18 @@ public class ClasspathLocator extends AbstractModProvider implements IModLocator
|
||||
var ret = new ArrayList<ModFileOrException>();
|
||||
for (var path : claimed) {
|
||||
// Filter out anything found by the ServiceLoader
|
||||
if (!ModDirTransformerDiscoverer.isServiceProvider(path))
|
||||
if (!ModDirTransformerDiscoverer.isServiceProvider(path)
|
||||
&& !JarInJarDependencyLocator.isOption(path))
|
||||
ret.add(createMod(path));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<URL> getUrls(ClassLoader cl, String resource) {
|
||||
private static List<URL> getUrls(ClassLoader cl, String resource) {
|
||||
try {
|
||||
var lst = Collections.list(cl.getResources(resource));
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) {
|
||||
Collections.sort(lst, (a, b) -> a.toString().compareTo(b.toString()));
|
||||
lst.sort(Comparator.comparing(URL::toString));
|
||||
LOGGER.debug(LogMarkers.SCAN, "Scanning Classloader: {} for {}", cl, resource);
|
||||
for (var url : lst)
|
||||
LOGGER.debug(LogMarkers.SCAN, "\t{}", url);
|
||||
@@ -104,11 +105,10 @@ public class ClasspathLocator extends AbstractModProvider implements IModLocator
|
||||
len += 2;
|
||||
}
|
||||
str = str.substring(0, str.length() - len);
|
||||
var path = Paths.get(URI.create(str));
|
||||
return path;
|
||||
return Path.of(URI.create(str));
|
||||
}
|
||||
|
||||
private static Path getPathFromResource(ClassLoader cl, String resource) {
|
||||
static Path getPathFromResource(ClassLoader cl, String resource) {
|
||||
var url = cl.getResource(resource);
|
||||
if (url == null)
|
||||
return null;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import net.minecraftforge.forgespi.coremod.ICoreModFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class CoreModFile implements ICoreModFile {
|
||||
private final Path internalPath;
|
||||
private final ModFile file;
|
||||
private final String name;
|
||||
|
||||
CoreModFile(final String name, final Path path, final ModFile file) {
|
||||
this.name = name;
|
||||
this.internalPath = path;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader readCoreMod() throws IOException {
|
||||
return Files.newBufferedReader(this.internalPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getPath() {
|
||||
return this.internalPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader getAdditionalFile(final String fileName) throws IOException {
|
||||
return Files.newBufferedReader(file.findResource(fileName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOwnerId() {
|
||||
return this.file.getModInfos().get(0).getModId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{Name: " + name + ", Owner: " + getOwnerId() + " @ " + getPath() + "}";
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -9,7 +9,6 @@ import cpw.mods.modlauncher.api.LamdbaExceptionUtils.Supplier_WithExceptions;
|
||||
import net.minecraftforge.fml.loading.StringUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.stream.Stream;
|
||||
@@ -25,7 +24,7 @@ public enum InvalidModIdentifier {
|
||||
LITELOADER(filePresent("litemod.json")),
|
||||
OPTIFINE(filePresent("optifine/Installer.class")),
|
||||
BUKKIT(filePresent("plugin.yml")),
|
||||
INVALIDZIP((f,zf) -> zf.isEmpty()); // note: only this one INVALIDZIP check is ran until the todo on this class is fixed
|
||||
INVALIDZIP((_,zf) -> zf.isEmpty()); // note: only this one INVALIDZIP check is ran until the todo on this class is fixed
|
||||
|
||||
private final BiPredicate<Path, Optional<ZipFile>> ident;
|
||||
|
||||
@@ -52,7 +51,7 @@ public enum InvalidModIdentifier {
|
||||
|
||||
private static BiPredicate<Path, Optional<ZipFile>> filePresent(String filename)
|
||||
{
|
||||
return (f, zfo) -> zfo.map(zf -> zf.getEntry(filename) != null).orElse(false);
|
||||
return (_, zfo) -> zfo.map(zf -> zf.getEntry(filename) != null).orElse(false);
|
||||
}
|
||||
|
||||
private static <T> Optional<T> optionalFromException(Supplier_WithExceptions<T, ? extends Exception> supp)
|
||||
|
||||
+622
-103
@@ -5,37 +5,60 @@
|
||||
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||
import com.electronwill.nightconfig.core.file.FileConfig;
|
||||
import com.mojang.logging.LogUtils;
|
||||
|
||||
import cpw.mods.modlauncher.Launcher;
|
||||
import cpw.mods.modlauncher.api.TypesafeMap;
|
||||
import net.minecraftforge.fml.loading.EarlyLoadingException;
|
||||
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import net.minecraftforge.forgespi.locating.IDependencyLocator;
|
||||
import net.minecraftforge.forgespi.locating.IModFile;
|
||||
import net.minecraftforge.forgespi.locating.ModFileLoadingException;
|
||||
import net.minecraftforge.forgespi.locating.IModFile.Type;
|
||||
import net.minecraftforge.jarjar.metadata.ContainedJarIdentifier;
|
||||
import net.minecraftforge.jarjar.metadata.ContainedJarMetadata;
|
||||
import net.minecraftforge.jarjar.metadata.ContainedVersion;
|
||||
import net.minecraftforge.jarjar.metadata.MetadataIOHandler;
|
||||
import net.minecraftforge.jarjar.selection.JarSelector;
|
||||
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||
|
||||
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
|
||||
import org.apache.maven.artifact.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.module.ModuleDescriptor;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class JarInJarDependencyLocator extends AbstractModProvider implements IDependencyLocator {
|
||||
private static final String ROIMFS = "roimfs";
|
||||
private static final String COLOR_CODE = "\u00a7";
|
||||
private static final String RESET = COLOR_CODE + "r";
|
||||
private static final String YELLOW = COLOR_CODE + "e";
|
||||
@@ -43,32 +66,238 @@ public class JarInJarDependencyLocator extends AbstractModProvider implements ID
|
||||
private static final String GREEN = COLOR_CODE + "2";
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final Marker MARKER = MarkerFactory.getMarker("JAR-JAR"); // LogMarkers.SCAN;
|
||||
|
||||
private static final Map<Path, Option> OPTIONS = new HashMap<>();
|
||||
private static volatile boolean optionsLoaded = false;
|
||||
|
||||
private static final Attributes.Name MIXIN_CONFIGS_ATTR = new Attributes.Name("MixinConfigs");
|
||||
private static final Attributes.Name MIXIN_CONNECTOR_ATTR = new Attributes.Name("MixinConnector");
|
||||
private static final ContainedJarMetadata MIXIN_EXTRAS_DEPENDENCY = makeMixinExtraDependency();
|
||||
|
||||
private static ContainedJarMetadata makeMixinExtraDependency() {
|
||||
VersionRange range = null;
|
||||
try {
|
||||
range = VersionRange.createFromVersionSpec("[0,)");
|
||||
} catch (InvalidVersionSpecificationException e) {
|
||||
throw new RuntimeException("Failed to create version range for mixinextras-forge dependency", e);
|
||||
}
|
||||
|
||||
return new ContainedJarMetadata(
|
||||
new ContainedJarIdentifier("io.github.llamalad7", "mixinextras-forge"),
|
||||
new ContainedVersion(range, null),
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "JarInJar";
|
||||
}
|
||||
|
||||
private int fsID = 0;
|
||||
private synchronized int nextId() {
|
||||
return fsID++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IModFile> scanMods(Iterable<IModFile> loadedMods) {
|
||||
final List<IModFile> sources = Lists.newArrayList();
|
||||
loadedMods.forEach(sources::add);
|
||||
var mods = new ArrayList<IModFile>();
|
||||
loadedMods.forEach(mods::add);
|
||||
|
||||
var dependenciesToLoad = JarSelector.detectAndSelect(
|
||||
sources,
|
||||
this::loadResourceFromModFile,
|
||||
this::loadModFileFrom,
|
||||
this::identifyMod,
|
||||
this::exception
|
||||
);
|
||||
var selector = new Selector(mods);
|
||||
|
||||
if (dependenciesToLoad.isEmpty()) {
|
||||
// Add a synthetic Mixin-Extras dependency if we detect anyone using Mixin
|
||||
if (anyMixinsLoaded(mods)) {
|
||||
if (!selector.isRequired(MIXIN_EXTRAS_DEPENDENCY.identifier()))
|
||||
selector.addRequirement(MIXIN_EXTRAS_DEPENDENCY);
|
||||
} else if (selector.entries.size() == mods.size()) {
|
||||
LOGGER.info("No dependencies to load found. Skipping!");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
LOGGER.info("Found {} dependencies adding them to mods collection", dependenciesToLoad.size());
|
||||
return dependenciesToLoad;
|
||||
loadOptions();
|
||||
|
||||
// Ideally, the json would be in order so we only have to loop once. But its written this way just in case.
|
||||
var seen = new HashSet<Option>();
|
||||
var added = false;
|
||||
do {
|
||||
added = false;
|
||||
for (var entry : OPTIONS.values()) {
|
||||
if (seen.contains(entry) || !selector.isRequired(entry.meta.identifier()))
|
||||
continue;
|
||||
|
||||
seen.add(entry);
|
||||
|
||||
entry.addTo(selector);
|
||||
|
||||
if (entry.deps != null && !entry.deps.isEmpty()) {
|
||||
added = true;
|
||||
for (var dep : entry.deps)
|
||||
selector.addRequirement(dep);
|
||||
}
|
||||
}
|
||||
} while (added);
|
||||
|
||||
var selected = selector.select();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
var ids = new TreeMap<String, List<Entry>>();
|
||||
for (var entry : selector.entries.values()) {
|
||||
if (entry != FAILED && entry.coord != null)
|
||||
ids.computeIfAbsent(entry.coord, _ -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
|
||||
for (var entry : ids.entrySet()) {
|
||||
LOGGER.info(MARKER, "JarJar Candidated for {}", entry.getKey());
|
||||
Collections.sort(entry.getValue());
|
||||
for (var option : entry.getValue())
|
||||
LOGGER.info(MARKER, "\t{}{}", selected.contains(option) ? '*' : ' ', option);
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.isEmpty()) {
|
||||
LOGGER.info("No dependencies to load found. Skipping!");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/* Since Java 11, the JRE decompresses all zip files to memory when getting a ByteChannel for them.
|
||||
* JarJar only targets Minecraft 1.18+ which is Java17+ Which means we can exploit the fact that
|
||||
* these are Disjointed file systems, and close all unselected libraries.
|
||||
*
|
||||
* https://github.com/openjdk/jdk11u/blob/25a9b9e5594e88c9e1e66f95c7ca45d5a9da8854/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java#L703
|
||||
*
|
||||
* Combined with the fact that the JarJarFileSystem has always simply called into FileSystems.newFileSystem for its wrapped targets.
|
||||
* https://github.com/MinecraftForge/JarJar/blame/b39b2051439c81519d826d38898778bf3081356f/src/main/java/net/minecraftforge/jarjar/nio/pathfs/PathFileSystem.java#L38
|
||||
*
|
||||
* This effectively means that JarJar has ALWAYS been a wrapper around in-memory copies of every jar it scans.
|
||||
*
|
||||
* It also never closes these FileSystems, which potentially means they are leaked if the GC can't inherently GC them.
|
||||
*
|
||||
* Ideally we would extract these contained jars to a temp folder, so that we can use the nice optimizations Java has for ZipFile/JarFile.
|
||||
* But in order to do that we would need to write the file to a place that could potentially have conflicts.
|
||||
* To resolve those conflicts we should take a hint out of maven/gradle/minecraft's book and key the filepath based on a hash of the file being extracted.
|
||||
* However, because that hash is no available in existing metadata we would need to re-calculate it.
|
||||
*
|
||||
* To do it in an efficient way we I needed to be able to share the single opened FileSystem with both the SecureJar implementation, and with our recursive search.
|
||||
* So I use my new ReadOnlyInMemoryFileSystem which is specifically designed to use the same backing array for the JarInJar search as it does with the resulting dependency
|
||||
* selected SecureJars.
|
||||
*
|
||||
* This means that if we wanted to we could rather efficiently calculate a hash for extracting the file. However, it also means that we don't have to extract to file to
|
||||
* support ZipFileSystem caching properly. So as long as we are okay with not using the JRE's optimized ZipFile implementation, or we want to support legacy JarInJar metadata,
|
||||
* using this efficient in memory file system is the best option performance wise. Extracting the files during launch without an efficient caching/unique identifier system
|
||||
* would incur a lot of Disc IO which can be super slow depending on your setup.
|
||||
*
|
||||
* So, it is a stated goal of our future re-write to extract files into a cache directory to gain the benefits of JRE optimizations, using a FileSystem much simpler then
|
||||
* the existing JarInJarFileSystem is good enough.
|
||||
*/
|
||||
|
||||
// Lets close all unselected FileSystems
|
||||
var wasSelected = new HashSet<>(selected);
|
||||
for (var entry : selector.entries.values())
|
||||
entry.cleanup(wasSelected.contains(entry));
|
||||
|
||||
LOGGER.info(MARKER, "Found {} dependencies adding them to mods collection", selected.size());
|
||||
var ret = new ArrayList<IModFile>(selected.size());
|
||||
|
||||
// Now lets build the actual mod files
|
||||
for (var entry : selected) {
|
||||
try {
|
||||
var root = entry.getFinalPath();
|
||||
var mod = createMod(root, false, entry.type.name());
|
||||
|
||||
if (mod.ex() != null)
|
||||
throw fail("Failed to load JarInJar file " + entry, mod.ex());
|
||||
|
||||
ret.add(mod.file());
|
||||
} catch (EarlyLoadingException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw fail("Failed to load JarInJar file " + entry, e);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Boolean anyMixinsLoaded = null;
|
||||
private static boolean anyMixinsLoaded(List<IModFile> mods) {
|
||||
var ret = anyMixinsLoaded;
|
||||
if (ret != null) return ret;
|
||||
|
||||
if (!FMLEnvironment.production
|
||||
&& Launcher.INSTANCE.blackboard().get(TypesafeMap.Key.getOrCreate(Launcher.INSTANCE.blackboard(), "isMixinEnabledInDev", Boolean.class)).orElse(false)) {
|
||||
anyMixinsLoaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var mod : mods) {
|
||||
var attributes = mod.getSecureJar().moduleDataProvider().getManifest().getMainAttributes();
|
||||
if (attributes.getValue(MIXIN_CONFIGS_ATTR) != null || attributes.getValue(MIXIN_CONNECTOR_ATTR) != null) {
|
||||
anyMixinsLoaded = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
anyMixinsLoaded = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
private record Options(List<OptionMetadata> options) {}
|
||||
private record OptionMetadata(String resource, Type layer, String id, List<ContainedJarMetadata> deps, ContainedJarMetadata meta, boolean nested) {}
|
||||
|
||||
private static void loadOptions() {
|
||||
if (optionsLoaded)
|
||||
return;
|
||||
|
||||
synchronized (OPTIONS) {
|
||||
if (optionsLoaded)
|
||||
return;
|
||||
|
||||
optionsLoaded = true;
|
||||
|
||||
var self = JarInJarDependencyLocator.class;
|
||||
|
||||
try (var stream = self.getModule().getResourceAsStream("/jarjar_options.json")) {
|
||||
if (stream == null) {
|
||||
LOGGER.error(MARKER, "Failed to find jarjar_options.json");
|
||||
return;
|
||||
}
|
||||
|
||||
var meta = MetadataIOHandler.getGson().fromJson(new InputStreamReader(stream), Options.class);
|
||||
//var meta = MetadataIOHandler.fromStream(stream).orElse(null);
|
||||
if (meta == null) {
|
||||
LOGGER.error(MARKER, "Corrupted jarjar_options.json");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var jar : meta.options()) {
|
||||
Path path = ClasspathLocator.getPathFromResource(self.getClassLoader(), jar.resource());
|
||||
if (path == null) {
|
||||
LOGGER.error(MARKER, "Failed to find JarJar option for {}", jar.resource());
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.debug(MARKER, "Found JarJar Option: {}", path.toAbsolutePath());
|
||||
if (jar.nested())
|
||||
OPTIONS.put(path, new NestedOption(jar.id(), path, jar.layer(), jar.meta(), jar.deps()));
|
||||
else
|
||||
OPTIONS.put(path, new Option(jar.id(), path, jar.layer(), jar.meta(), jar.deps()));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error(MARKER, "Failed to read JarJar Options file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isOption(Path path) {
|
||||
loadOptions();
|
||||
return OPTIONS.containsKey(path);
|
||||
}
|
||||
|
||||
private static EarlyLoadingException fail(String message, @Nullable Throwable cause) {
|
||||
LOGGER.error(MARKER, message);
|
||||
return new EarlyLoadingException(message, cause, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,89 +305,7 @@ public class JarInJarDependencyLocator extends AbstractModProvider implements ID
|
||||
return IModFile.Type.GAMELIBRARY.name();
|
||||
}
|
||||
|
||||
protected Optional<IModFile> loadModFileFrom(IModFile file, Path path) {
|
||||
try {
|
||||
final Path pathInModFile = file.findResource(path.toString());
|
||||
final URI filePathUri = new URI("jij:" + (pathInModFile.toAbsolutePath().toUri().getRawSchemeSpecificPart())).normalize();
|
||||
final Map<String, ?> outerFsArgs = ImmutableMap.of("packagePath", pathInModFile);
|
||||
final FileSystem zipFS = FileSystems.newFileSystem(filePathUri, outerFsArgs);
|
||||
final Path pathInFS = zipFS.getPath("/");
|
||||
final IModFile.Type parentType = file.getType();
|
||||
final String modType;
|
||||
if (parentType == IModFile.Type.LIBRARY || parentType == IModFile.Type.LANGPROVIDER) {
|
||||
modType = IModFile.Type.LIBRARY.name();
|
||||
} else {
|
||||
modType = IModFile.Type.GAMELIBRARY.name();
|
||||
}
|
||||
return Optional.of(createMod(pathInFS, false, modType).file());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to load mod file {} from {}", path, file.getFileName());
|
||||
final RuntimeException exception = new ModFileLoadingException("Failed to load mod file " + file.getFileName());
|
||||
exception.initCause(e);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
protected EarlyLoadingException exception(Collection<JarSelector.ResolutionFailureInformation<IModFile>> failedDependencies) {
|
||||
final List<EarlyLoadingException.ExceptionData> errors = failedDependencies.stream()
|
||||
.filter(entry -> !entry.sources().isEmpty()) //Should never be the case, but just to be sure
|
||||
.map(this::buildExceptionData)
|
||||
.toList();
|
||||
|
||||
return new EarlyLoadingException(failedDependencies.size() + " Dependency restrictions were not met.", null, errors);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private EarlyLoadingException.ExceptionData buildExceptionData(JarSelector.ResolutionFailureInformation<IModFile> entry) {
|
||||
return new EarlyLoadingException.ExceptionData(
|
||||
getErrorTranslationKey(entry),
|
||||
entry.identifier().group() + ":" + entry.identifier().artifact(),
|
||||
entry.sources()
|
||||
.stream()
|
||||
.flatMap(this::getModWithVersionRangeStream)
|
||||
.map(this::formatError)
|
||||
.collect(Collectors.joining(", "))
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getErrorTranslationKey(JarSelector.ResolutionFailureInformation<IModFile> entry) {
|
||||
return entry.failureReason() == JarSelector.FailureReason.VERSION_RESOLUTION_FAILED ?
|
||||
"fml.dependencyloading.conflictingdependencies" :
|
||||
"fml.dependencyloading.mismatchedcontaineddependencies";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Stream<ModWithVersionRange> getModWithVersionRangeStream(JarSelector.SourceWithRequestedVersionRange<IModFile> file) {
|
||||
return file.sources()
|
||||
.stream()
|
||||
.map(IModFile::getModFileInfo)
|
||||
.flatMap(modFileInfo -> modFileInfo.getMods().stream())
|
||||
.map(modInfo -> new ModWithVersionRange(modInfo, file.requestedVersionRange(), file.includedVersion()));
|
||||
}
|
||||
|
||||
protected Optional<InputStream> loadResourceFromModFile(IModFile modFile, Path path) {
|
||||
try {
|
||||
var pathInModFile = modFile.findResource(path.toString());
|
||||
if (!Files.exists(pathInModFile)) {
|
||||
LOGGER.debug("Failed to load resource {} from {}, it does not contain dependency information.", path, modFile.getFileName());
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Files.newInputStream(pathInModFile));
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to load resource {} from mod {}, cause {}", path, modFile.getFileName(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String formatError(ModWithVersionRange modWithVersionRange){
|
||||
return YELLOW + modWithVersionRange.modInfo().getModId() + RESET + " - " +
|
||||
RED + modWithVersionRange.versionRange().toString() + RESET + " - " +
|
||||
GREEN + modWithVersionRange.artifactVersion().toString() + RESET;
|
||||
}
|
||||
|
||||
protected String identifyMod(IModFile modFile) {
|
||||
protected static String identifyMod(IModFile modFile) {
|
||||
if (modFile.getModFileInfo() != null && !modFile.getModInfos().isEmpty())
|
||||
return modFile.getModInfos().stream().map(IModInfo::getModId).collect(Collectors.joining());
|
||||
|
||||
@@ -169,5 +316,377 @@ public class JarInJarDependencyLocator extends AbstractModProvider implements ID
|
||||
return modFile.getFileName();
|
||||
}
|
||||
|
||||
private record ModWithVersionRange(IModInfo modInfo, VersionRange versionRange, ArtifactVersion artifactVersion) {}
|
||||
private static final Entry FAILED = new Entry("FAILED ENTRY", Path.of(""), Type.LIBRARY, null);
|
||||
private static sealed class Entry implements Comparable<Entry> {
|
||||
final String id;
|
||||
final Path path;
|
||||
final Type type;
|
||||
final @Nullable String coord;
|
||||
|
||||
private Entry(String id, Path path, Type type, @Nullable String coord) {
|
||||
this.id = id;
|
||||
this.path = path;
|
||||
this.type = type;
|
||||
this.coord = coord;
|
||||
}
|
||||
|
||||
Path getFinalPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
void cleanup(boolean selected) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (path.getFileName() == null)
|
||||
return path.toUri().toString();
|
||||
return path.getFileName().toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Path getResource(String path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Entry o) {
|
||||
return o.toString().compareTo(toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ModEntry extends Entry {
|
||||
private final IModFile mod;
|
||||
|
||||
ModEntry(IModFile mod) {
|
||||
super(identifyMod(mod), mod.getFilePath(), mod.getType(), null);
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
Path getResource(String path) {
|
||||
return mod.findResource(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static sealed class Option extends Entry {
|
||||
final ContainedJarMetadata meta;
|
||||
final List<ContainedJarMetadata> deps;
|
||||
|
||||
private Option(String id, Path path, Type type, ContainedJarMetadata meta, List<ContainedJarMetadata> deps) {
|
||||
super(id, path, type, meta.identifier().group() + ':' + meta.identifier().artifact());
|
||||
this.meta = meta;
|
||||
this.deps = deps;
|
||||
}
|
||||
|
||||
public void addTo(Selector selector) {
|
||||
selector.option(this, meta);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NestedOption extends Option {
|
||||
private FileSystem zip;
|
||||
private Path root;
|
||||
|
||||
private NestedOption(String id, Path path, Type type, ContainedJarMetadata meta, List<ContainedJarMetadata> deps) {
|
||||
super(id, path, type, meta, deps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTo(Selector selector) {
|
||||
super.addTo(selector);
|
||||
selector.add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
void cleanup(boolean selected) {
|
||||
if (this.zip != null) {
|
||||
try {
|
||||
zip.close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(MARKER, "Failed to close unselected FileSystem {}", this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Path getResource(String path) {
|
||||
if (zip == null) {
|
||||
try {
|
||||
this.zip = FileSystems.newFileSystem(this.path);
|
||||
this.root = zip.getRootDirectories().iterator().next();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(MARKER, "Failed to open FileSystem for option root {}", this, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var target = root.resolve(path);
|
||||
if (Files.exists(target))
|
||||
return target;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class Nested extends Entry {
|
||||
private final Entry parent;
|
||||
private final String nestedPath;
|
||||
private final Path zipPath;
|
||||
|
||||
Nested(String id, Path path, Type type, String coords, Entry parent, String nestedPath, Path zipPath) {
|
||||
super(id, path, type, coords);
|
||||
this.parent = parent;
|
||||
this.nestedPath = nestedPath;
|
||||
this.zipPath = zipPath;
|
||||
}
|
||||
|
||||
/*
|
||||
* Due to a bug in Mixin, anything that is not on the DefaultFileSystem MUST be a directory.
|
||||
* Or else it can't resolve resources correctly. So for anything we put on the roimfs we need
|
||||
* to return the nested zip file system.
|
||||
*
|
||||
* This will be an annoying to backport, but we can cross that bridge when we come to it.
|
||||
*
|
||||
* See: https://github.com/SpongePowered/Mixin/blob/4053421aa10aaac6127d969028a29c94fe3054f6/src/main/java/org/spongepowered/asm/launch/platform/MainAttributes.java#L102
|
||||
*/
|
||||
@Override
|
||||
public Path getFinalPath() {
|
||||
return zipPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
void cleanup(boolean selected) {
|
||||
if (selected)
|
||||
return;
|
||||
|
||||
LOGGER.info(MARKER, "Closeing unselected FileSystem {}", this);
|
||||
try {
|
||||
zipPath.getFileSystem().close();
|
||||
path.getFileSystem().close();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(MARKER, "Failed to close unselected FileSystem {}", this, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
Path getResource(String path) {
|
||||
return zipPath.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent + "!/" + nestedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private record Key(Entry parent, String path) {}
|
||||
|
||||
private final class Selector extends JarSelector<Entry> {
|
||||
private final HashMap<Key, Entry> entries = new HashMap<>();
|
||||
private final HashMap<Entry, HashMap<String, String>> children = new HashMap<>();
|
||||
|
||||
private Selector(Iterable<IModFile> mods) {
|
||||
for (var mod : mods)
|
||||
this.force(new ModEntry(mod));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void force(Entry entry) {
|
||||
super.force(entry);
|
||||
entries.put(new Key(entry, ""), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void option(Entry entry, ContainedJarMetadata meta) {
|
||||
super.option(entry, meta);
|
||||
entries.put(new Key(entry, ""), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Entry entry) {
|
||||
super.add(entry);
|
||||
entries.put(new Key(entry, ""), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected InputStream getResource(Entry source, String path) {
|
||||
try {
|
||||
Path target = source.getResource(path);
|
||||
if (target == null || !Files.exists(target)) {
|
||||
LOGGER.debug(MARKER, "Failed to load resource {} from {}, it does not contain dependency information.", path, source);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the metadata file and store identifiers so we can group them later.
|
||||
if (JarSelector.CONTAINED_JARS_METADATA_PATH.equals(path)) {
|
||||
try (var is = Files.newInputStream(target)) {
|
||||
var meta = MetadataIOHandler.fromStream(is).orElse(null);
|
||||
if (meta == null)
|
||||
return Files.newInputStream(target);
|
||||
var children = new HashMap<String, String>();
|
||||
for (var child : meta.jars())
|
||||
children.put(child.path().replace('\\', '/'), child.identifier().group() + ':' + child.identifier().artifact());
|
||||
this.children.put(source, children);
|
||||
}
|
||||
}
|
||||
|
||||
return Files.newInputStream(target);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(MARKER, "Failed to load resource {} from {}, cause {}", path, source, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Entry getNested(Entry source, String path) {
|
||||
var cleaned = path.replace('\\', '/');
|
||||
var key = new Key(source, cleaned);
|
||||
var entry = entries.get(key);
|
||||
if (entry == null) {
|
||||
entry = createNested(source, path, cleaned);
|
||||
if (entry == null)
|
||||
entry = FAILED;
|
||||
entries.put(key, entry);
|
||||
}
|
||||
return entry == FAILED ? null : entry;
|
||||
}
|
||||
|
||||
private Entry createNested(Entry source, String path, String cleaned) {
|
||||
var targetPath = source.getResource(path);
|
||||
|
||||
if (targetPath == null || !Files.isRegularFile(targetPath))
|
||||
return null;
|
||||
|
||||
Path roimfsPath = null;
|
||||
Path zipPath = null;
|
||||
try {
|
||||
// Lets copy the uncompressed file to memory, and then create a ReadOnlyInMemorryFileSystem for it, this allows us to reference it by URI.
|
||||
// Which is needed because a lot of things require navigation via URI.
|
||||
// And ZipFileSystem only caches the FileSystem instance when accessed via URI.
|
||||
byte[] data = Files.readAllBytes(targetPath);
|
||||
String filename = targetPath.getFileName().toString();
|
||||
URI uri = new URI(ROIMFS, "jar-jar-" + nextId() + "/" + filename, null);
|
||||
var roimfs = FileSystems.newFileSystem(uri, Collections.singletonMap("data", data));
|
||||
roimfsPath = roimfs.getPath(filename);
|
||||
var zip = FileSystems.newFileSystem(new URI("jar:" + roimfsPath.toUri()), Collections.emptyMap());
|
||||
zipPath = zip.getRootDirectories().iterator().next(); // Get first root, which on ZipFileSystem is the only root
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
LOGGER.error(MARKER, "Failed to create FileSystem for nested jar {}", source, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
var mf = new Manifest();
|
||||
var mfPath = zipPath.resolve(JarFile.MANIFEST_NAME);
|
||||
if (Files.exists(mfPath)) {
|
||||
try (var is = Files.newInputStream(mfPath)) {
|
||||
mf = new Manifest(is);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(MARKER, "Failed to read manifest from {}", source, e);
|
||||
}
|
||||
}
|
||||
|
||||
IModFile.Type type = null;
|
||||
String id = null;
|
||||
var tomlPath = zipPath.resolve(MODS_TOML);
|
||||
if (Files.exists(tomlPath)) {
|
||||
var cfg = FileConfig.builder(tomlPath).build();
|
||||
cfg.load();
|
||||
cfg.close();
|
||||
if (cfg.getOrElse("mods", null) instanceof Collection collection) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var mods = (Collection<UnmodifiableConfig>)collection;
|
||||
if (!mods.isEmpty()) {
|
||||
var first = mods.iterator().next();
|
||||
var modId = first.getOrElse("modId", null);
|
||||
if (modId instanceof String str)
|
||||
id = str;
|
||||
}
|
||||
}
|
||||
var value = mf.getMainAttributes().getValue(ModFile.TYPE);
|
||||
if (value != null)
|
||||
type = IModFile.Type.valueOf(value);
|
||||
else
|
||||
type = IModFile.Type.MOD;
|
||||
} else {
|
||||
var value = mf.getMainAttributes().getValue(ModFile.TYPE);
|
||||
if (value != null)
|
||||
type = IModFile.Type.valueOf(value);
|
||||
else {
|
||||
// No mods.toml, no entry value, so decide based on the container's type
|
||||
if (source.type == IModFile.Type.LIBRARY || source.type == IModFile.Type.LANGPROVIDER)
|
||||
type = IModFile.Type.LIBRARY;
|
||||
else
|
||||
type = IModFile.Type.GAMELIBRARY;
|
||||
}
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
var modulePath = zipPath.resolve(MODULE_INFO);
|
||||
if (Files.exists(modulePath)) {
|
||||
try (var is = Files.newInputStream(modulePath)) {
|
||||
var module = ModuleDescriptor.read(is);
|
||||
id = module.name();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(MARKER, "Failed to read module-info.class from {}", source, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (id == null)
|
||||
id = mf.getMainAttributes().getValue("Automatic-Module-Name");
|
||||
|
||||
if (id == null)
|
||||
id = Path.of(path).getFileName().toString();
|
||||
|
||||
String coords = null;
|
||||
var child = this.children.get(source);
|
||||
if (child != null)
|
||||
coords = child.get(cleaned);
|
||||
|
||||
return new Nested(id, roimfsPath, type, coords, source, cleaned, zipPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getIdentifier(Entry entry) {
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Throwable getFailureException(Collection<ResolutionFailureInformation<Entry>> failures) {
|
||||
var errors = new ArrayList<ExceptionData>();
|
||||
for (var failure : failures) {
|
||||
var message = failure.failureReason() == JarSelector.FailureReason.VERSION_RESOLUTION_FAILED ? "fml.dependencyloading.conflictingdependencies" : "fml.dependencyloading.mismatchedcontaineddependencies";
|
||||
var joiner = new StringJoiner(", ");
|
||||
for (var source : failure.sources()) {
|
||||
String paths = null;
|
||||
if (source.sources().size() == 0) {
|
||||
paths = "[No Sources]";
|
||||
} else if (source.sources().size() == 1) {
|
||||
var entry = source.sources().iterator().next();
|
||||
paths = entry.toString();
|
||||
} else {
|
||||
var j = new StringJoiner(", ", "[", "]");
|
||||
for (var entry : source.sources())
|
||||
j.add(entry.toString());
|
||||
paths = j.toString();
|
||||
}
|
||||
|
||||
var error = YELLOW + paths + RESET + " - " +
|
||||
RED + source.requestedVersionRange() + RESET + " - " +
|
||||
GREEN + source.includedVersion() + RESET;
|
||||
joiner.add(error);
|
||||
}
|
||||
|
||||
errors.add(new ExceptionData(message, failure.identifier().group() + ":" + failure.identifier().artifact(), joiner.toString()));
|
||||
}
|
||||
|
||||
return new EarlyLoadingException(failures.size() + " Dependency restrictions were not met.", null, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -14,28 +14,27 @@ import net.minecraftforge.forgespi.locating.IModLocator;
|
||||
import net.minecraftforge.forgespi.locating.ModFileFactory;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class MinecraftLocator extends AbstractModProvider implements IModLocator {
|
||||
public final class MinecraftLocator extends AbstractModProvider implements IModLocator {
|
||||
@Override
|
||||
public List<IModLocator.ModFileOrException> scanMods() {
|
||||
var minecraft = FMLLoader.getLaunchHandler().getMinecraftPaths();
|
||||
var paths = minecraft.toArray(Path[]::new);
|
||||
|
||||
// Minecraft itself.
|
||||
var meta = new ModJarMetadata();
|
||||
var mcjar = SecureJar.from(
|
||||
jar -> meta,
|
||||
minecraft.toArray(Path[]::new)
|
||||
);
|
||||
var mc = ModFileFactory.FACTORY.build(mcjar, this, this::buildMinecraftTOML);
|
||||
var mcjar = SecureJar.from(_ -> meta, paths);
|
||||
var mc = ModFileFactory.FACTORY.build(mcjar, this, MinecraftLocator::buildMinecraftTOML);
|
||||
meta.setModFile(mc);
|
||||
|
||||
return List.of(new ModFileOrException(mc, null));
|
||||
}
|
||||
|
||||
private IModFileInfo buildMinecraftTOML(final IModFile iModFile) {
|
||||
private static IModFileInfo buildMinecraftTOML(final IModFile iModFile) {
|
||||
// We haven't changed this in years, and I can't be asked right now to special case this one file in the path.
|
||||
final var conf = Config.inMemory();
|
||||
conf.set("modLoader", "minecraft");
|
||||
@@ -45,18 +44,23 @@ public class MinecraftLocator extends AbstractModProvider implements IModLocator
|
||||
mods.set("modId", "minecraft");
|
||||
mods.set("version", FMLLoader.versionInfo().mcVersion());
|
||||
mods.set("displayName", "Minecraft");
|
||||
mods.set("logoFile", "mcplogo.png");
|
||||
mods.set("credits", "Mojang, deobfuscated by MCP");
|
||||
mods.set("authors", "MCP: Searge,ProfMobius,IngisKahn,Fesh0r,ZeuX,R4wk,LexManos,Bspkrs");
|
||||
mods.set("description", "Minecraft, decompiled and deobfuscated with MCP technology");
|
||||
//mods.set("logoFile", "mcplogo.png");
|
||||
//mods.set("credits", "Mojang, deobfuscated by MCP");
|
||||
//mods.set("authors", "MCP: Searge,ProfMobius,IngisKahn,Fesh0r,ZeuX,R4wk,LexManos,Bspkrs");
|
||||
mods.set("description", "The base Minecraft game, published by Mojang decompiled and deobfuscated by Forge");
|
||||
conf.set("mods", List.of(mods));
|
||||
|
||||
var configWrapper = new NightConfigWrapper(conf);
|
||||
return new ModFileInfo((ModFile)iModFile, configWrapper, configWrapper::setFile, List.of());
|
||||
return new ModFileInfo((ModFile) iModFile, configWrapper, configWrapper::setFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "minecraft";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanFile(IModFile file, Consumer<Path> pathConsumer) {
|
||||
// skip annotation scanning for MC itself
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -19,7 +19,7 @@ import org.objectweb.asm.Type;
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
@ApiStatus.Internal
|
||||
class ModAnnotation {
|
||||
final class ModAnnotation {
|
||||
public static ModFileScanData.AnnotationData fromModAnnotation(Type clazz, ModAnnotation annotation) {
|
||||
return new ModFileScanData.AnnotationData(annotation.asmType, annotation.type, clazz, annotation.member, annotation.values);
|
||||
}
|
||||
@@ -84,6 +84,7 @@ class ModAnnotation {
|
||||
}
|
||||
|
||||
public void endArray() {
|
||||
arrayList.trimToSize();
|
||||
values.put(arrayName, arrayList);
|
||||
arrayList = null;
|
||||
}
|
||||
|
||||
+50
-3
@@ -22,7 +22,47 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@ApiStatus.Internal
|
||||
class ModClassVisitor extends ClassVisitor {
|
||||
final class ModClassVisitor extends ClassVisitor {
|
||||
private static final Set<String> ANNOTATION_BLACKLIST;
|
||||
|
||||
private static final Type OBJECT_TYPE = Type.getObjectType("java/lang/Object");
|
||||
private static final Type RECORD_TYPE = Type.getObjectType("java/lang/Record");
|
||||
|
||||
static {
|
||||
var customBlacklist = System.getProperty("forge.annotationScanningBlacklist");
|
||||
if (customBlacklist != null) {
|
||||
ANNOTATION_BLACKLIST = Set.of(customBlacklist.split(","));
|
||||
} else {
|
||||
ANNOTATION_BLACKLIST = Set.of(
|
||||
"Ljava/lang/Deprecated;",
|
||||
"Ljava/lang/FunctionalInterface;",
|
||||
"Ljava/lang/SafeVarargs;",
|
||||
|
||||
"Ljava/lang/annotation/Retention;",
|
||||
"Ljava/lang/annotation/Target;",
|
||||
|
||||
"Ljavax/annotation/Nullable;",
|
||||
|
||||
"Lorg/jspecify/nullness/NonNull;",
|
||||
"Lorg/jspecify/nullness/Nullable;",
|
||||
"Lorg/jspecify/annotations/NullMarked;",
|
||||
"Lorg/jspecify/annotations/NullUnmarked;",
|
||||
|
||||
"Lorg/jetbrains/annotations/ApiStatus$Internal;",
|
||||
"Lorg/jetbrains/annotations/Contract;",
|
||||
"Lorg/jetbrains/annotations/Nullable;",
|
||||
"Lorg/jetbrains/annotations/NotNull;",
|
||||
"Lorg/jetbrains/annotations/VisibleForTesting;",
|
||||
|
||||
"Lcom/google/common/annotations/VisibleForTesting;",
|
||||
|
||||
"Lnet/minecraftforge/api/distmarker/OnlyIn", // should not be used by mods
|
||||
// @Mod.EventBusSubscriber doesn't use scan data for finding @SubscribeEvent annotated methods
|
||||
"Lnet/minecraftforge/eventbus/api/listener/SubscribeEvent;"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Type asmType;
|
||||
private Type asmSuperType;
|
||||
private Set<Type> interfaces;
|
||||
@@ -35,12 +75,17 @@ class ModClassVisitor extends ClassVisitor {
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||
this.asmType = Type.getObjectType(name);
|
||||
this.asmSuperType = superName != null && !superName.isEmpty() ? Type.getObjectType(superName) : null;
|
||||
this.asmSuperType = superName != null && !superName.isEmpty() ? switch (superName) {
|
||||
case "java/lang/Object" -> OBJECT_TYPE;
|
||||
case "java/lang/Record" -> RECORD_TYPE;
|
||||
default -> Type.getObjectType(superName);
|
||||
} : null;
|
||||
this.interfaces = Stream.of(interfaces).map(Type::getObjectType).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(final String annotationName, final boolean runtimeVisible) {
|
||||
if (ANNOTATION_BLACKLIST.contains(annotationName)) return null;
|
||||
var ann = new ModAnnotation(ElementType.TYPE, Type.getType(annotationName), this.asmType.getClassName());
|
||||
annotations.addFirst(ann);
|
||||
return new ModAnnotationVisitor(ann);
|
||||
@@ -51,6 +96,7 @@ class ModClassVisitor extends ClassVisitor {
|
||||
return new FieldVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String annotationName, boolean runtimeVisible) {
|
||||
if (ANNOTATION_BLACKLIST.contains(annotationName)) return null;
|
||||
var ann = new ModAnnotation(ElementType.FIELD, Type.getType(annotationName), name);
|
||||
annotations.addFirst(ann);
|
||||
return new ModAnnotationVisitor(ann);
|
||||
@@ -63,6 +109,7 @@ class ModClassVisitor extends ClassVisitor {
|
||||
return new MethodVisitor(Opcodes.ASM9) {
|
||||
@Override
|
||||
public AnnotationVisitor visitAnnotation(String annotationName, boolean runtimeVisible) {
|
||||
if (ANNOTATION_BLACKLIST.contains(annotationName)) return null;
|
||||
ModAnnotation ann = new ModAnnotation(ElementType.METHOD, Type.getType(annotationName), name + desc);
|
||||
annotations.addFirst(ann);
|
||||
return new ModAnnotationVisitor(ann);
|
||||
@@ -78,7 +125,7 @@ class ModClassVisitor extends ClassVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
private class ModAnnotationVisitor extends AnnotationVisitor {
|
||||
private final class ModAnnotationVisitor extends AnnotationVisitor {
|
||||
private final ModAnnotation annotation;
|
||||
private boolean array;
|
||||
private boolean isSubAnnotation;
|
||||
|
||||
+45
-51
@@ -5,8 +5,6 @@
|
||||
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.modlauncher.Launcher;
|
||||
import cpw.mods.modlauncher.api.IModuleLayerManager;
|
||||
@@ -28,6 +26,7 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -35,38 +34,37 @@ import java.util.ServiceLoader;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class ModDiscoverer {
|
||||
public final class ModDiscoverer {
|
||||
private ModDiscoverer() {}
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final ServiceLoader<IModLocator> modLocators;
|
||||
private final ServiceLoader<IDependencyLocator> dependencyLocators;
|
||||
private final List<IModLocator> modLocatorList;
|
||||
private final List<IDependencyLocator> dependencyLocatorList;
|
||||
|
||||
public ModDiscoverer(Map<String, ?> arguments) {
|
||||
Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), v->ModsFolderLocator::new);
|
||||
Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v-> StartupNotificationManager.locatorConsumer().orElseGet(()-> s->{}));
|
||||
final var moduleLayerManager = Launcher.INSTANCE.environment().findModuleLayerManager().orElseThrow();
|
||||
modLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class);
|
||||
dependencyLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IDependencyLocator.class);
|
||||
modLocatorList = ServiceLoaderUtils.streamServiceLoader(()-> modLocators, sce->LOGGER.error("Failed to load mod locator list", sce)).collect(Collectors.toList());
|
||||
modLocatorList.forEach(l->l.initArguments(arguments));
|
||||
dependencyLocatorList = ServiceLoaderUtils.streamServiceLoader(()-> dependencyLocators, sce->LOGGER.error("Failed to load dependency locator list", sce)).collect(Collectors.toList());
|
||||
dependencyLocatorList.forEach(l->l.initArguments(arguments));
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.CORE))
|
||||
{
|
||||
@SuppressWarnings("removal")
|
||||
public static ModValidator discoverMods(Map<String, ?> arguments) {
|
||||
var env = Launcher.INSTANCE.environment();
|
||||
env.computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), _ -> ModsFolderLocator::new);
|
||||
env.computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), _ -> StartupNotificationManager.locatorConsumer().orElse(_ -> {}));
|
||||
final var moduleLayerManager = env.findModuleLayerManager().orElseThrow();
|
||||
ServiceLoader<IModLocator> modLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class);
|
||||
ServiceLoader<IDependencyLocator> dependencyLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IDependencyLocator.class);
|
||||
List<IModLocator> modLocatorList = ServiceLoaderUtils.streamWithErrorHandling(modLocators, sce -> LOGGER.error("Failed to load mod locator list", sce)).toList();
|
||||
for (IModLocator iModLocator : modLocatorList) {
|
||||
iModLocator.initArguments(arguments);
|
||||
}
|
||||
List<IDependencyLocator> dependencyLocatorList = ServiceLoaderUtils.streamWithErrorHandling(dependencyLocators, sce -> LOGGER.error("Failed to load dependency locator list", sce)).toList();
|
||||
for (IDependencyLocator l : dependencyLocatorList) {
|
||||
l.initArguments(arguments);
|
||||
}
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.CORE)) {
|
||||
LOGGER.debug(LogMarkers.CORE, "Found Mod Locators : {}", modLocatorList.stream()
|
||||
.map(modLocator -> "(%s:%s)".formatted(modLocator.name(),
|
||||
modLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(",")));
|
||||
}
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.CORE))
|
||||
{
|
||||
LOGGER.debug(LogMarkers.CORE, "Found Dependency Locators : {}", dependencyLocatorList.stream()
|
||||
.map(dependencyLocator -> "(%s:%s)".formatted(dependencyLocator.name(),
|
||||
dependencyLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(",")));
|
||||
}
|
||||
}
|
||||
.map(modLocator -> "(%s:%s)".formatted(modLocator.name(),
|
||||
modLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(",")));
|
||||
|
||||
LOGGER.debug(LogMarkers.CORE, "Found Dependency Locators : {}", dependencyLocatorList.stream()
|
||||
.map(dependencyLocator -> "(%s:%s)".formatted(dependencyLocator.name(),
|
||||
dependencyLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
public ModValidator discoverMods() {
|
||||
LOGGER.debug(LogMarkers.SCAN,"Scanning for mods and other resources to load. We know {} ways to find mods", modLocatorList.size());
|
||||
List<ModFile> loadedFiles = new ArrayList<>();
|
||||
List<EarlyLoadingException.ExceptionData> discoveryErrorData = new ArrayList<>();
|
||||
@@ -114,7 +112,7 @@ public class ModDiscoverer {
|
||||
}
|
||||
|
||||
LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} valid mod files", locator, locatedFiles.size());
|
||||
handleLocatedFiles(loadedFiles, locatedFiles);
|
||||
handleLocatedFiles(loadedFiles, locatedFiles, locator);
|
||||
} catch (InvalidModFileException imfe) {
|
||||
// We don't generally expect this exception, since it should come from the candidates stream above and be handled in the Locator, but just in case.
|
||||
LOGGER.error(LogMarkers.SCAN, "Locator {} found an invalid mod file {}", locator, imfe.getBrokenFile(), imfe);
|
||||
@@ -126,15 +124,14 @@ public class ModDiscoverer {
|
||||
}
|
||||
|
||||
//First processing run of the mod list. Any duplicates will cause resolution failure and dependency loading will be skipped.
|
||||
Map<IModFile.Type, List<ModFile>> modFilesMap = Maps.newHashMap();
|
||||
Map<IModFile.Type, List<ModFile>> modFilesMap = new EnumMap<>(IModFile.Type.class);
|
||||
try {
|
||||
final UniqueModListBuilder modsUniqueListBuilder = new UniqueModListBuilder(loadedFiles);
|
||||
final UniqueModListBuilder.UniqueModListData uniqueModsData = modsUniqueListBuilder.buildUniqueList();
|
||||
final UniqueModListBuilder.UniqueModListData uniqueModsData = UniqueModListBuilder.buildUniqueList(loadedFiles);
|
||||
|
||||
//Grab the temporary results.
|
||||
//This allows loading to continue to a base state, in case dependency loading fails.
|
||||
modFilesMap = uniqueModsData.modFiles().stream()
|
||||
.collect(Collectors.groupingBy(IModFile::getType));
|
||||
.collect(Collectors.groupingBy(IModFile::getType, () -> new EnumMap<>(IModFile.Type.class), Collectors.toList()));
|
||||
loadedFiles = uniqueModsData.modFiles();
|
||||
}
|
||||
catch (EarlyLoadingException exception) {
|
||||
@@ -149,14 +146,10 @@ public class ModDiscoverer {
|
||||
for (IDependencyLocator locator : dependencyLocatorList) {
|
||||
try {
|
||||
LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator);
|
||||
final List<IModFile> locatedMods = ImmutableList.copyOf(loadedFiles);
|
||||
final List<IModFile> locatedMods = List.copyOf(loadedFiles);
|
||||
|
||||
var locatedFiles = locator.scanMods(locatedMods);
|
||||
if (locatedFiles.stream().anyMatch(file -> !(file instanceof ModFile))) {
|
||||
LOGGER.error(LogMarkers.SCAN, "A dependency locator returned a file which is not a ModFile instance!. They will be skipped!");
|
||||
}
|
||||
|
||||
handleLocatedFiles(loadedFiles, locatedFiles);
|
||||
handleLocatedFiles(loadedFiles, locatedFiles, locator);
|
||||
}
|
||||
catch (EarlyLoadingException exception) {
|
||||
LOGGER.error(LogMarkers.SCAN, "Failed to load dependencies with locator {}", locator, exception);
|
||||
@@ -166,16 +159,15 @@ public class ModDiscoverer {
|
||||
|
||||
//Second processing run of the mod list. Any duplicates will cause resolution failure and only the mods list will be loaded.
|
||||
try {
|
||||
final UniqueModListBuilder modsAndDependenciesUniqueListBuilder = new UniqueModListBuilder(loadedFiles);
|
||||
final UniqueModListBuilder.UniqueModListData uniqueModsAndDependenciesData = modsAndDependenciesUniqueListBuilder.buildUniqueList();
|
||||
final UniqueModListBuilder.UniqueModListData uniqueModsAndDependenciesData = UniqueModListBuilder.buildUniqueList(loadedFiles);
|
||||
|
||||
//We now only need the mod files map, not the list.
|
||||
modFilesMap = uniqueModsAndDependenciesData.modFiles().stream()
|
||||
.collect(Collectors.groupingBy(IModFile::getType));
|
||||
.collect(Collectors.groupingBy(IModFile::getType, () -> new EnumMap<>(IModFile.Type.class), Collectors.toList()));
|
||||
} catch (EarlyLoadingException exception) {
|
||||
LOGGER.error(LogMarkers.SCAN, "Failed to build unique mod list after dependency discovery.", exception);
|
||||
discoveryErrorData.addAll(exception.getAllData());
|
||||
modFilesMap = loadedFiles.stream().collect(Collectors.groupingBy(IModFile::getType));
|
||||
modFilesMap = loadedFiles.stream().collect(Collectors.groupingBy(IModFile::getType, () -> new EnumMap<>(IModFile.Type.class), Collectors.toList()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -190,12 +182,14 @@ public class ModDiscoverer {
|
||||
return validator;
|
||||
}
|
||||
|
||||
private void handleLocatedFiles(final List<ModFile> loadedFiles, final List<IModFile> locatedFiles)
|
||||
{
|
||||
var locatedModFiles = locatedFiles.stream().filter(ModFile.class::isInstance).map(ModFile.class::cast).toList();
|
||||
for (IModFile mf : locatedModFiles) {
|
||||
LOGGER.info(LogMarkers.SCAN, "Found mod file {} of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider());
|
||||
private static void handleLocatedFiles(final List<ModFile> loadedFiles, final List<IModFile> locatedFiles, final Object locator) {
|
||||
for (IModFile mf : locatedFiles) {
|
||||
if (mf instanceof ModFile modFile) {
|
||||
LOGGER.info(LogMarkers.SCAN, "Found mod file {} of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider());
|
||||
loadedFiles.add(modFile);
|
||||
} else {
|
||||
LOGGER.error(LogMarkers.SCAN, "Skipping mod file {} found by {}, as it was not a ModFile instance!", mf.getFileName(), locator);
|
||||
}
|
||||
}
|
||||
loadedFiles.addAll(locatedModFiles);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package net.minecraftforge.fml.loading.moddiscovery;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.jarhandling.SecureJar;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.LanguageLoadingProvider;
|
||||
import net.minecraftforge.fml.loading.LogMarkers;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
@@ -21,13 +21,12 @@ import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -39,19 +38,16 @@ import java.util.jar.Manifest;
|
||||
public class ModFile implements IModFile {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final String jarVersion;
|
||||
private final ModFileFactory.ModFileInfoParser parser;
|
||||
private Map<String, Object> fileProperties;
|
||||
private List<IModLanguageProvider> loaders;
|
||||
private Throwable scanError;
|
||||
private final SecureJar jar;
|
||||
private final Type modFileType;
|
||||
private final Manifest manifest;
|
||||
private final IModProvider provider;
|
||||
private IModFileInfo modFileInfo;
|
||||
private final IModFileInfo modFileInfo;
|
||||
private ModFileScanData fileModFileScanData;
|
||||
private CompletableFuture<ModFileScanData> futureScanResult;
|
||||
private List<CoreModFile> coreMods;
|
||||
private Path accessTransformer;
|
||||
private List<Path> accessTransformers = List.of();
|
||||
|
||||
static final Attributes.Name TYPE = new Attributes.Name("FMLModType");
|
||||
private SecureJar.Status securityStatus;
|
||||
@@ -63,12 +59,11 @@ public class ModFile implements IModFile {
|
||||
public ModFile(final SecureJar jar, final IModProvider provider, final ModFileFactory.ModFileInfoParser parser, String type) {
|
||||
this.provider = provider;
|
||||
this.jar = jar;
|
||||
this.parser = parser;
|
||||
|
||||
manifest = this.jar.moduleDataProvider().getManifest();
|
||||
var manifest = this.jar.moduleDataProvider().getManifest();
|
||||
modFileType = Type.valueOf(type);
|
||||
jarVersion = Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)).orElse("0.0NONE");
|
||||
this.modFileInfo = ModFileParser.readModList(this, this.parser);
|
||||
this.modFileInfo = ModFileParser.readModList(this, parser);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,22 +90,30 @@ public class ModFile implements IModFile {
|
||||
return modFileInfo.getMods();
|
||||
}
|
||||
|
||||
public Optional<Path> getAccessTransformer() {
|
||||
return Optional.ofNullable(Files.exists(accessTransformer) ? accessTransformer : null);
|
||||
public List<Path> getAccessTransformers() {
|
||||
return this.accessTransformers;
|
||||
}
|
||||
|
||||
public boolean identifyMods() {
|
||||
this.modFileInfo = ModFileParser.readModList(this, this.parser);
|
||||
if (this.modFileInfo == null) return this.getType() != Type.MOD;
|
||||
LOGGER.debug(LogMarkers.LOADING,"Loading mod file {} with languages {}", this.getFilePath(), this.modFileInfo.requiredLanguageLoaders());
|
||||
this.coreMods = ModFileParser.getCoreMods(this);
|
||||
this.coreMods.forEach(mi-> LOGGER.debug(LogMarkers.LOADING,"Found coremod {}", mi.getPath()));
|
||||
this.accessTransformer = findResource("META-INF", "accesstransformer.cfg");
|
||||
return true;
|
||||
}
|
||||
LOGGER.debug(LogMarkers.LOADING,"Loading mod file {} with languages {}", this.getFileName(), this.modFileInfo.requiredLanguageLoaders());
|
||||
var cfg = this.modFileInfo.getConfig()
|
||||
.<List<String>>getConfigElement("accessTransformers")
|
||||
.orElse(null);
|
||||
|
||||
public List<CoreModFile> getCoreMods() {
|
||||
return coreMods;
|
||||
if (cfg == null) {
|
||||
var path = findResource("META-INF", "accesstransformer.cfg");
|
||||
if (Files.exists(path)) {
|
||||
this.accessTransformers = List.of(path);
|
||||
}
|
||||
} else if (!cfg.isEmpty()) {
|
||||
var paths = new ArrayList<Path>(cfg.size());
|
||||
for (var path : cfg)
|
||||
paths.add(getSecureJar().getPath(path.replace('\\', '/')));
|
||||
this.accessTransformers = List.copyOf(paths);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,22 +173,37 @@ public class ModFile implements IModFile {
|
||||
|
||||
public void identifyLanguage() {
|
||||
var lst = new ArrayList<IModLanguageProvider>(this.modFileInfo.requiredLanguageLoaders().size());
|
||||
var services = FMLLoader.getLanguageLoadingProvider();
|
||||
for (var spec : this.modFileInfo.requiredLanguageLoaders()) {
|
||||
var service = services.findLanguage(this, spec.languageName(), spec.acceptedVersions());
|
||||
var service = LanguageLoadingProvider.findLanguage(this, spec.languageName(), spec.acceptedVersions());
|
||||
lst.add(service);
|
||||
}
|
||||
this.loaders = Collections.unmodifiableList(lst);
|
||||
this.loaders = List.copyOf(lst);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Mod File: " + Objects.toString(this.jar.getPrimaryPath());
|
||||
var path = getFilePath();
|
||||
return "Mod File: " + path == null ? "null" : path.toUri().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return getFilePath().getFileName().toString();
|
||||
// This is only used for debugging, so lets try and make it a useful debug
|
||||
var path = getFilePath();
|
||||
var filename = path.getFileName();
|
||||
|
||||
// ZipFileSystem toString will return the outer file path
|
||||
var ret = filename == null ? path.getFileSystem().toString() : filename.toString();
|
||||
|
||||
// If it's a directory on the default file system, return the full path
|
||||
if (Files.isDirectory(path) && path.getFileSystem() == FileSystems.getDefault())
|
||||
ret = path.toAbsolutePath().toString();
|
||||
|
||||
// If we are in a FileSystem 'root' like UnionFS, return the full uri
|
||||
if (ret.isEmpty() || ret.equals("/"))
|
||||
ret = path.toUri().toString();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+93
-70
@@ -7,7 +7,6 @@ package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
|
||||
import net.minecraftforge.fml.loading.LogMarkers;
|
||||
import net.minecraftforge.fml.loading.StringUtils;
|
||||
import net.minecraftforge.forgespi.language.IConfigurable;
|
||||
@@ -18,17 +17,15 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -37,10 +34,8 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
{
|
||||
public class ModFileInfo implements IModFileInfo, IConfigurable {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final String MAYBE_NOT_JAVAFML_VER = "[2,)";
|
||||
private static final String JAVAFML = "javafml";
|
||||
@@ -60,8 +55,7 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
private final String license;
|
||||
private final List<String> usesServices;
|
||||
|
||||
ModFileInfo(final ModFile modFile, final IConfigurable config, Consumer<IModFileInfo> configFileConsumer)
|
||||
{
|
||||
ModFileInfo(final ModFile modFile, final IConfigurable config, Consumer<IModFileInfo> configFileConsumer) {
|
||||
this.modFile = modFile;
|
||||
this.config = config;
|
||||
configFileConsumer.accept(this);
|
||||
@@ -86,10 +80,6 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
.orElse(false);
|
||||
this.clientSideOnly = config.<Boolean>getConfigElement("clientSideOnly")
|
||||
.orElse(false);
|
||||
if (this.clientSideOnly) {
|
||||
// if clientSideOnly, then this is definitely a Forge mod
|
||||
maybeNotAForgeMod = false;
|
||||
}
|
||||
this.showAsDataPack = config.<Boolean>getConfigElement("showAsDataPack")
|
||||
.orElse(false);
|
||||
this.usesServices = config.<List<String>>getConfigElement("services")
|
||||
@@ -98,6 +88,10 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
.map(StringUtils::toURL)
|
||||
.orElse(null);
|
||||
|
||||
// if clientSideOnly, then this is definitely a Forge mod
|
||||
if (this.clientSideOnly)
|
||||
maybeNotAForgeMod = false;
|
||||
|
||||
if (maybeNotAForgeMod) {
|
||||
// mark this file as not a Forge mod. Note: this marker may be removed later based on the mod dependencies
|
||||
this.properties = config.<Map<String, Object>>getConfigElement("properties")
|
||||
@@ -110,15 +104,14 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
this.modFile.setFileProperties(this.properties);
|
||||
|
||||
final List<? extends IConfigurable> modConfigs = config.getConfigList("mods");
|
||||
if (modConfigs.isEmpty())
|
||||
{
|
||||
if (modConfigs == null || modConfigs.isEmpty())
|
||||
throw new InvalidModFileException("Missing mods list", this);
|
||||
}
|
||||
|
||||
this.mods = modConfigs.stream()
|
||||
.map(mi-> (IModInfo)new ModInfo(this, mi))
|
||||
.map(mi -> (IModInfo) ModInfo.of(this, mi))
|
||||
.toList();
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.LOADING))
|
||||
{
|
||||
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.LOADING)) {
|
||||
LOGGER.debug(LogMarkers.LOADING, "Found valid mod file {} with {} mods - versions {}",
|
||||
this.modFile.getFileName(),
|
||||
this.mods.stream().map(IModInfo::getModId).collect(Collectors.joining(",", "{", "}")),
|
||||
@@ -132,13 +125,11 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IModInfo> getMods()
|
||||
{
|
||||
public List<IModInfo> getMods() {
|
||||
return mods;
|
||||
}
|
||||
|
||||
public ModFile getFile()
|
||||
{
|
||||
public ModFile getFile() {
|
||||
return this.modFile;
|
||||
}
|
||||
|
||||
@@ -148,44 +139,42 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFileProperties()
|
||||
{
|
||||
public Map<String, Object> getFileProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showAsResourcePack()
|
||||
{
|
||||
public boolean showAsResourcePack() {
|
||||
return this.showAsResourcePack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClientSideOnly()
|
||||
{
|
||||
public boolean isClientSideOnly() {
|
||||
return this.clientSideOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showAsDataPack()
|
||||
{
|
||||
public boolean showAsDataPack() {
|
||||
return this.showAsDataPack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getConfigElement(final String... key)
|
||||
{
|
||||
public <T> Optional<T> getConfigElement(final String key) {
|
||||
return this.config.getConfigElement(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(final String... key)
|
||||
{
|
||||
public <T> Optional<T> getConfigElement(final String... key) {
|
||||
return this.config.getConfigElement(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(final String... key) {
|
||||
return this.config.getConfigList(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLicense()
|
||||
{
|
||||
public String getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
@@ -194,63 +183,97 @@ public class ModFileInfo implements IModFileInfo, IConfigurable
|
||||
return this;
|
||||
}
|
||||
|
||||
public URL getIssueURL()
|
||||
{
|
||||
public URL getIssueURL() {
|
||||
return issueURL;
|
||||
}
|
||||
|
||||
public boolean missingLicense()
|
||||
{
|
||||
public boolean missingLicense() {
|
||||
return Strings.isNullOrEmpty(license);
|
||||
}
|
||||
|
||||
private static final char[] HEX = "0123456789ABCDEF".toCharArray();
|
||||
public Optional<String> getCodeSigningFingerprint() {
|
||||
var signers = this.modFile.getSecureJar().getManifestSigners();
|
||||
return (signers == null ? Stream.<CodeSigner>of() : Arrays.stream(signers))
|
||||
.flatMap(csa->csa.getSignerCertPath().getCertificates().stream())
|
||||
.findFirst()
|
||||
.map(LamdbaExceptionUtils.rethrowFunction(Certificate::getEncoded))
|
||||
.map(bytes->LamdbaExceptionUtils.uncheck(()->MessageDigest.getInstance("SHA-256")).digest(bytes))
|
||||
.map(StringUtils::binToHex)
|
||||
.map(str-> String.join(":", str.split("(?<=\\G.{2})")));
|
||||
if (signers == null)
|
||||
return Optional.empty();
|
||||
|
||||
for (var signer : signers) {
|
||||
for (var cert : signer.getSignerCertPath().getCertificates()) {
|
||||
try {
|
||||
var encoded = cert.getEncoded();
|
||||
var sha256 = MessageDigest.getInstance("SHA-256");
|
||||
var digest = sha256.digest(encoded);
|
||||
var buf = new StringBuilder(digest.length * 3);
|
||||
for (int x = 0; x < digest.length - 1; x++) {
|
||||
buf.append(HEX[(digest[x] & 0xF0) >> 4]);
|
||||
buf.append(HEX[(digest[x] & 0x0F) ]);
|
||||
buf.append(':');
|
||||
}
|
||||
buf.append(HEX[(digest[digest.length - 1] & 0xF0) >> 4]);
|
||||
buf.append(HEX[(digest[digest.length - 1] & 0x0F) ]);
|
||||
|
||||
// We only care about the first. Should we list them all?
|
||||
return Optional.of(buf.toString());
|
||||
} catch (CertificateEncodingException | NoSuchAlgorithmException e) {
|
||||
sneak(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
|
||||
}
|
||||
|
||||
public Optional<String> getTrustData() {
|
||||
return Arrays.stream(this.modFile.getSecureJar().getManifestSigners())
|
||||
.flatMap(csa->csa.getSignerCertPath().getCertificates().stream())
|
||||
.findFirst()
|
||||
.map(X509Certificate.class::cast)
|
||||
.map(c->{
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(c.getSubjectX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
|
||||
boolean selfSigned = false;
|
||||
try {
|
||||
c.verify(c.getPublicKey());
|
||||
selfSigned = true;
|
||||
} catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
|
||||
// not self signed
|
||||
}
|
||||
if (selfSigned) {
|
||||
var signers = this.modFile.getSecureJar().getManifestSigners();
|
||||
if (signers == null)
|
||||
return Optional.empty();
|
||||
|
||||
for (var signer : signers) {
|
||||
for (var cert : signer.getSignerCertPath().getCertificates()) {
|
||||
if (!(cert instanceof X509Certificate x509))
|
||||
continue;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.append(x509.getSubjectX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
|
||||
|
||||
var selfSigned = false;
|
||||
try {
|
||||
x509.verify(x509.getPublicKey());
|
||||
selfSigned = true;
|
||||
} catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) {
|
||||
// not self signed
|
||||
}
|
||||
|
||||
if (selfSigned)
|
||||
sb.append(" self-signed");
|
||||
} else {
|
||||
sb.append(" signed by ").append(c.getIssuerX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
|
||||
};
|
||||
return sb.toString();
|
||||
});
|
||||
else
|
||||
sb.append(" signed by ").append(x509.getIssuerX500Principal().getName(X500Principal.RFC2253).split(",")[0]);
|
||||
|
||||
return Optional.of(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String moduleName() {
|
||||
return getMods().get(0).getModId();
|
||||
return this.modFile.getSecureJar().name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String versionString() {
|
||||
return getMods().get(0).getVersion().toString();
|
||||
return getMods().getFirst().getVersion().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> usesServices() {
|
||||
return usesServices;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E extends Throwable, R> R sneak(Throwable e) throws E {
|
||||
throw (E)e;
|
||||
}
|
||||
}
|
||||
-28
@@ -6,8 +6,6 @@
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.FileConfig;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.loading.LogMarkers;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
@@ -16,13 +14,8 @@ import net.minecraftforge.forgespi.locating.ModFileFactory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ModFileParser {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
@@ -46,25 +39,4 @@ public class ModFileParser {
|
||||
final NightConfigWrapper configWrapper = new NightConfigWrapper(fileConfig);
|
||||
return new ModFileInfo(modFile, configWrapper, configWrapper::setFile);
|
||||
}
|
||||
|
||||
protected static List<CoreModFile> getCoreMods(final ModFile modFile) {
|
||||
Map<String,String> coreModPaths;
|
||||
try {
|
||||
final Path coremodsjson = modFile.findResource("META-INF", "coremods.json");
|
||||
if (!Files.exists(coremodsjson)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final Type type = new TypeToken<Map<String, String>>() {}.getType();
|
||||
final Gson gson = new Gson();
|
||||
coreModPaths = gson.fromJson(Files.newBufferedReader(coremodsjson), type);
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug(LogMarkers.LOADING,"Failed to read coremod list coremods.json", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return coreModPaths.entrySet().stream()
|
||||
.peek(e-> LOGGER.debug(LogMarkers.LOADING,"Found coremod {} with Javascript path {}", e.getKey(), e.getValue()))
|
||||
.map(e -> new CoreModFile(e.getKey(), modFile.findResource(e.getValue()),modFile))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
+153
-198
@@ -19,202 +19,189 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class ModInfo implements IModInfo, IConfigurable {
|
||||
public record ModInfo(
|
||||
ModFileInfo getOwningFile,
|
||||
IConfigurable getConfig,
|
||||
|
||||
String getModId,
|
||||
String getNamespace,
|
||||
ArtifactVersion getVersion,
|
||||
|
||||
String getDisplayName,
|
||||
String getDescription,
|
||||
Optional<String> getLogoFile,
|
||||
boolean getLogoBlur,
|
||||
Optional<URL> getUpdateURL,
|
||||
Optional<URL> getModURL,
|
||||
|
||||
Holder<List<? extends ModVersion>> dependencies,
|
||||
Holder<List<ForgeFeature.Bound>> forgeFeatures,
|
||||
Map<String, Object> getModProperties
|
||||
) implements IModInfo, IConfigurable {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final DefaultArtifactVersion DEFAULT_VERSION = new DefaultArtifactVersion("1");
|
||||
private static final Pattern VALID_MODID = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
|
||||
private static final Pattern VALID_NAMESPACE = Pattern.compile("^[a-z][a-z0-9_.-]{1,63}$");
|
||||
private static final Pattern VALID_VERSION = Pattern.compile("^\\d+.*");
|
||||
|
||||
private final ModFileInfo owningFile;
|
||||
private final String modId;
|
||||
private final String namespace;
|
||||
private final ArtifactVersion version;
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
private final Optional<String> logoFile;
|
||||
private final boolean logoBlur;
|
||||
private final Optional<URL> updateJSONURL;
|
||||
private final List<? extends IModInfo.ModVersion> dependencies;
|
||||
|
||||
private final List<ForgeFeature.Bound> features;
|
||||
private final Map<String,Object> properties;
|
||||
private final IConfigurable config;
|
||||
private final Optional<URL> modUrl;
|
||||
|
||||
public ModInfo(final ModFileInfo owningFile, final IConfigurable config)
|
||||
{
|
||||
Optional<ModFileInfo> ownFile = Optional.ofNullable(owningFile);
|
||||
this.owningFile = owningFile;
|
||||
this.config = config;
|
||||
public static ModInfo of(ModFileInfo owningFile, IConfigurable config) {
|
||||
// These are sourced from the mod specific [[mod]] entry
|
||||
this.modId = config.<String>getConfigElement("modId")
|
||||
.orElseThrow(() -> new InvalidModFileException("Missing modId", owningFile));
|
||||
String modId = config.<String>getConfigElement("modId").orElse(null);
|
||||
if (modId == null)
|
||||
throw new InvalidModFileException("Missing modId", owningFile);
|
||||
|
||||
// verify we have a valid modid
|
||||
if (!VALID_MODID.matcher(this.modId).matches()) {
|
||||
LOGGER.error(LogUtils.FATAL_MARKER, "Invalid modId found in file {} - {} does not match the standard: {}", this.owningFile.getFile().getFilePath(), this.modId, VALID_MODID.pattern());
|
||||
throw new InvalidModFileException("Invalid modId found : " + this.modId, owningFile);
|
||||
if (!VALID_MODID.matcher(modId).matches()) {
|
||||
LOGGER.error(LogUtils.FATAL_MARKER, "Invalid modId found in file {} - {} does not match the standard: {}", owningFile.getFile().getFilePath(), modId, VALID_MODID.pattern());
|
||||
throw new InvalidModFileException("Invalid modId found : " + modId, owningFile);
|
||||
}
|
||||
this.namespace = config.<String>getConfigElement("namespace")
|
||||
.orElse(this.modId);
|
||||
|
||||
String namespace = config.<String>getConfigElement("namespace")
|
||||
.orElse(modId);
|
||||
|
||||
// verify our namespace is valid
|
||||
if (!VALID_NAMESPACE.matcher(this.namespace).matches()) {
|
||||
LOGGER.error(LogUtils.FATAL_MARKER, "Invalid override namespace found in file {} - {} does not match the standard: {}", this.owningFile.getFile().getFilePath(), this.namespace, VALID_NAMESPACE.pattern());
|
||||
throw new InvalidModFileException("Invalid override namespace found : " + this.namespace, owningFile);
|
||||
if (!VALID_NAMESPACE.matcher(namespace).matches()) {
|
||||
LOGGER.error(LogUtils.FATAL_MARKER, "Invalid override namespace found in file {} - {} does not match the standard: {}", owningFile.getFile().getFilePath(), namespace, VALID_NAMESPACE.pattern());
|
||||
throw new InvalidModFileException("Invalid override namespace found : " + namespace, owningFile);
|
||||
}
|
||||
this.version = config.<String>getConfigElement("version")
|
||||
.map(s -> StringSubstitutor.replace(s, ownFile.map(ModFileInfo::getFile).orElse(null)))
|
||||
|
||||
ArtifactVersion version = config.<String>getConfigElement("version")
|
||||
.map(s -> StringSubstitutor.replace(s, owningFile.getFile()))
|
||||
.map(DefaultArtifactVersion::new)
|
||||
.orElse(DEFAULT_VERSION);
|
||||
|
||||
// verify we have a valid mod version
|
||||
if (!VALID_VERSION.matcher(this.version.toString()).matches()) {
|
||||
throw new InvalidModFileException("Illegal version number specified "+this.version, this.getOwningFile());
|
||||
}
|
||||
if (!VALID_VERSION.matcher(version.toString()).matches())
|
||||
throw new InvalidModFileException("Illegal version number specified " + version, owningFile);
|
||||
|
||||
// The remaining properties all default to sensible values and are not essential
|
||||
this.displayName = config.<String>getConfigElement("displayName")
|
||||
.orElse(this.modId);
|
||||
this.description = config.<String>getConfigElement("description")
|
||||
String displayName = config.<String>getConfigElement("displayName")
|
||||
.orElse(modId);
|
||||
|
||||
String description = config.<String>getConfigElement("description")
|
||||
.orElse("MISSING DESCRIPTION")
|
||||
.replace("\r\n", "\n").stripIndent();
|
||||
this.logoFile = Optional.ofNullable(config.<String>getConfigElement("logoFile")
|
||||
.orElseGet(() -> ownFile.flatMap(mf -> mf.<String>getConfigElement("logoFile"))
|
||||
.orElse(null)));
|
||||
this.logoBlur = config.<Boolean>getConfigElement("logoBlur")
|
||||
.orElseGet(() -> ownFile.flatMap(f -> f.<Boolean>getConfigElement("logoBlur"))
|
||||
.orElse(true));
|
||||
this.updateJSONURL = config.<String>getConfigElement("updateJSONURL")
|
||||
|
||||
Optional<String> logoFile = Optional.ofNullable(
|
||||
config.<String>getConfigElement("logoFile")
|
||||
.orElseGet(() -> owningFile.<String>getConfigElement("logoFile").orElse(null))
|
||||
);
|
||||
|
||||
Boolean logoBlur = config.<Boolean>getConfigElement("logoBlur").orElse(null);
|
||||
if (logoBlur == null)
|
||||
logoBlur = owningFile.<Boolean>getConfigElement("logoBlur").orElse(true);
|
||||
|
||||
Optional<URL> updateJSONURL = config.<String>getConfigElement("updateJSONURL")
|
||||
.map(StringUtils::toURL);
|
||||
this.modUrl = config.<String>getConfigElement("modUrl")
|
||||
|
||||
Optional<URL> modUrl = config.<String>getConfigElement("modUrl")
|
||||
.map(StringUtils::toURL);
|
||||
// These are sourced from the file rather than the mod-specific block, but with a modid tag
|
||||
this.dependencies = ownFile.map(mfi -> mfi.getConfigList("dependencies", this.modId))
|
||||
.orElse(Collections.emptyList())
|
||||
.stream()
|
||||
.map(dep -> new ModVersion(this, dep))
|
||||
.toList();
|
||||
this.features = ownFile.flatMap(mfi -> mfi.<Map<String, Object>>getConfigElement("features", this.modId))
|
||||
.stream()
|
||||
.flatMap(m->m.entrySet().stream())
|
||||
.map(this::makeBound)
|
||||
.toList();
|
||||
this.properties = ownFile.flatMap(mfi -> mfi.<Map<String, Object>>getConfigElement("modproperties", this.modId))
|
||||
|
||||
// dependencies and features are done after the constructor as they need to reference the ModInfo we are creating
|
||||
List<? extends ModVersion> dependencies = Collections.emptyList();
|
||||
List<ForgeFeature.Bound> forgeFeatures = Collections.emptyList();
|
||||
|
||||
Map<String, Object> modProperties = owningFile.<Map<String, Object>>getConfigElement("modproperties", modId)
|
||||
.map(Collections::unmodifiableMap)
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
return new ModInfo(
|
||||
owningFile, config,
|
||||
modId, namespace, version,
|
||||
displayName, description, logoFile, logoBlur, updateJSONURL, modUrl,
|
||||
new Holder<>(dependencies), new Holder<>(forgeFeatures), modProperties
|
||||
).setupDependencies().setupForgeFeatures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModFileInfo getOwningFile() {
|
||||
return owningFile;
|
||||
private ModInfo setupDependencies() {
|
||||
var deps = getOwningFile.getConfigList("dependencies", getModId);
|
||||
if (deps == null || deps.isEmpty()) {
|
||||
dependencies.value = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
|
||||
var tmp = new ModVersion[deps.size()];
|
||||
for (int i = 0; i < deps.size(); i++) {
|
||||
tmp[i] = ModVersion.of(this, deps.get(i));
|
||||
}
|
||||
dependencies.value = List.of(tmp);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModId() {
|
||||
return modId;
|
||||
}
|
||||
private ModInfo setupForgeFeatures() {
|
||||
var feats = getOwningFile.<Map<String, Object>>getConfigElement("features", getModId).orElse(null);
|
||||
if (feats == null) {
|
||||
forgeFeatures.value = Collections.emptyList();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName()
|
||||
{
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArtifactVersion getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IModInfo.ModVersion> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
return this.namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getModProperties() {
|
||||
return this.properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<URL> getUpdateURL() {
|
||||
return this.updateJSONURL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getLogoFile()
|
||||
{
|
||||
return this.logoFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getLogoBlur()
|
||||
{
|
||||
return this.logoBlur;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IConfigurable getConfig() {
|
||||
var tmp = new ArrayList<ForgeFeature.Bound>();
|
||||
for (var entry : feats.entrySet()) {
|
||||
if (!(entry.getValue() instanceof String val))
|
||||
throw new InvalidModFileException("Invalid feature bound {" + entry.getValue() + "} for key {" + entry.getKey() + "} only strings are accepted", getOwningFile);
|
||||
tmp.add(new ForgeFeature.Bound(entry.getKey(), val, this));
|
||||
}
|
||||
forgeFeatures.value = List.copyOf(tmp);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends ForgeFeature.Bound> getForgeFeatures() {
|
||||
return this.features;
|
||||
public <T> Optional<T> getConfigElement(String key) {
|
||||
return getConfig.getConfigElement(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getConfigElement(final String... key) {
|
||||
return this.config.getConfigElement(key);
|
||||
public <T> Optional<T> getConfigElement(String... key) {
|
||||
return getConfig.getConfigElement(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(final String... key) {
|
||||
public List<? extends IConfigurable> getConfigList(String... key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<URL> getModURL() {
|
||||
return modUrl;
|
||||
public List<? extends IModInfo.ModVersion> getDependencies() {
|
||||
return dependencies.value;
|
||||
}
|
||||
|
||||
private ForgeFeature.Bound makeBound(Map.Entry<String, Object> e) {
|
||||
if (e.getValue() instanceof String val) {
|
||||
return new ForgeFeature.Bound(e.getKey(), val, this);
|
||||
} else {
|
||||
throw new InvalidModFileException("Invalid feature bound {" + e.getValue() + "} for key {" + e.getKey() + "} only strings are accepted", this.owningFile);
|
||||
@Override
|
||||
public List<? extends ForgeFeature.Bound> getForgeFeatures() {
|
||||
return forgeFeatures.value;
|
||||
}
|
||||
|
||||
private static final class Holder<T> {
|
||||
private T value;
|
||||
|
||||
public Holder(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class ModVersion implements net.minecraftforge.forgespi.language.IModInfo.ModVersion {
|
||||
private IModInfo owner;
|
||||
private final String modId;
|
||||
private final VersionRange versionRange;
|
||||
private final boolean mandatory;
|
||||
private final Ordering ordering;
|
||||
private final DependencySide side;
|
||||
private final Optional<URL> referralUrl;
|
||||
private record ModVersion(
|
||||
Holder<IModInfo> owner,
|
||||
|
||||
public ModVersion(final IModInfo owner, final IConfigurable config) {
|
||||
this.owner = owner;
|
||||
this.modId = config.<String>getConfigElement("modId")
|
||||
.orElseThrow(()->new InvalidModFileException("Missing required field modid in dependency", getOwningFile()));
|
||||
if (this.modId.equals("forge")) {
|
||||
String getModId,
|
||||
VersionRange getVersionRange,
|
||||
boolean isMandatory,
|
||||
Ordering getOrdering,
|
||||
DependencySide getSide,
|
||||
Optional<URL> getReferralURL
|
||||
) implements IModInfo.ModVersion {
|
||||
public static ModVersion of(IModInfo owner, IConfigurable config) {
|
||||
var modId = config.<String>getConfigElement("modId").orElse(null);
|
||||
if (modId == null)
|
||||
throw new InvalidModFileException("Missing required field modid in dependency", owner.getOwningFile());
|
||||
|
||||
if (modId.equals("forge")) {
|
||||
var fileProps = owner.getOwningFile().getFileProperties();
|
||||
// Checking containsKey to avoid a possible exception if the property is not present (due to Collections.emptyMap())
|
||||
if (!fileProps.isEmpty() && fileProps.containsKey(ModFileInfo.NOT_A_FORGE_MOD_PROP)) {
|
||||
@@ -223,74 +210,42 @@ public class ModInfo implements IModInfo, IConfigurable {
|
||||
fileProps.remove(ModFileInfo.NOT_A_FORGE_MOD_PROP);
|
||||
}
|
||||
}
|
||||
var mandatory = config.<Boolean>getConfigElement("mandatory");
|
||||
if (mandatory.isPresent()) {
|
||||
this.mandatory = mandatory.get();
|
||||
} else if (owner.getOwningFile().getFileProperties().containsKey(ModFileInfo.NOT_A_FORGE_MOD_PROP)) {
|
||||
this.mandatory = true;
|
||||
} else {
|
||||
throw new InvalidModFileException("Missing required field mandatory in dependency", getOwningFile());
|
||||
}
|
||||
this.versionRange = config.<String>getConfigElement("versionRange")
|
||||
|
||||
boolean mandatory;
|
||||
var mandatoryValue = config.<Boolean>getConfigElement("mandatory");
|
||||
if (mandatoryValue.isPresent())
|
||||
mandatory = mandatoryValue.get();
|
||||
else if (owner.getOwningFile().getFileProperties().containsKey(ModFileInfo.NOT_A_FORGE_MOD_PROP))
|
||||
mandatory = true;
|
||||
else
|
||||
throw new InvalidModFileException("Missing required field mandatory in dependency", owner.getOwningFile());
|
||||
|
||||
var versionRange = config.<String>getConfigElement("versionRange")
|
||||
.map(MavenVersionAdapter::createFromVersionSpec)
|
||||
.orElse(UNBOUNDED);
|
||||
this.ordering = config.<String>getConfigElement("ordering")
|
||||
var ordering = config.<String>getConfigElement("ordering")
|
||||
.map(Ordering::valueOf)
|
||||
.orElse(Ordering.NONE);
|
||||
this.side = config.<String>getConfigElement("side")
|
||||
var side = config.<String>getConfigElement("side")
|
||||
.map(DependencySide::valueOf)
|
||||
.orElse(DependencySide.BOTH);
|
||||
this.referralUrl = config.<String>getConfigElement("referralUrl")
|
||||
var referralUrl = config.<String>getConfigElement("referralUrl")
|
||||
.map(StringUtils::toURL);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getModId()
|
||||
{
|
||||
return modId;
|
||||
return new ModVersion(
|
||||
new Holder<>(owner),
|
||||
modId, versionRange, mandatory, ordering, side, referralUrl
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionRange getVersionRange()
|
||||
{
|
||||
return versionRange;
|
||||
public IModInfo getOwner() {
|
||||
return owner.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMandatory()
|
||||
{
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ordering getOrdering()
|
||||
{
|
||||
return ordering;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DependencySide getSide()
|
||||
{
|
||||
return side;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOwner(final IModInfo owner)
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IModInfo getOwner()
|
||||
{
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<URL> getReferralURL() {
|
||||
return referralUrl;
|
||||
public void setOwner(IModInfo newOwner) {
|
||||
owner.value = newOwner;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+31
-16
@@ -9,49 +9,64 @@ import cpw.mods.jarhandling.JarMetadata;
|
||||
import net.minecraftforge.forgespi.locating.IModFile;
|
||||
import java.lang.module.ModuleDescriptor;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public final class ModJarMetadata implements JarMetadata {
|
||||
private static final String AUTOMATIC_MODULE_NAME = "Automatic-Module-Name";
|
||||
private IModFile modFile;
|
||||
private String name;
|
||||
private String version;
|
||||
private ModuleDescriptor descriptor;
|
||||
|
||||
ModJarMetadata() {
|
||||
}
|
||||
ModJarMetadata() { }
|
||||
|
||||
public void setModFile(IModFile file) {
|
||||
this.modFile = file;
|
||||
var mods = this.modFile.getModFileInfo().getMods();
|
||||
|
||||
if (!mods.isEmpty()) {
|
||||
var main = mods.get(0);
|
||||
this.name = main.getModId();
|
||||
this.version = main.getVersion().toString();
|
||||
}
|
||||
|
||||
var jar = file.getSecureJar();
|
||||
var aname = jar.moduleDataProvider().getManifest().getMainAttributes().getValue(AUTOMATIC_MODULE_NAME);
|
||||
if (aname != null)
|
||||
this.name = aname;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return modFile.getModFileInfo().moduleName();
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return modFile.getModFileInfo().versionString();
|
||||
return this.version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModuleDescriptor descriptor() {
|
||||
if (descriptor != null) return descriptor;
|
||||
if (descriptor != null)
|
||||
return descriptor;
|
||||
|
||||
var bld = ModuleDescriptor.newAutomaticModule(name())
|
||||
.version(version())
|
||||
.packages(modFile.getSecureJar().getPackages());
|
||||
modFile.getSecureJar().getProviders().stream()
|
||||
.filter(p -> !p.providers().isEmpty())
|
||||
.forEach(p -> bld.provides(p.serviceName(), p.providers()));
|
||||
modFile.getModFileInfo().usesServices().forEach(bld::uses);
|
||||
.version(version())
|
||||
.packages(modFile.getSecureJar().getPackages());
|
||||
|
||||
for (var provider : modFile.getSecureJar().getProviders()) {
|
||||
if (provider.providers().isEmpty())
|
||||
continue;
|
||||
|
||||
bld.provides(provider.serviceName(), provider.providers());
|
||||
}
|
||||
|
||||
descriptor = bld.build();
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public IModFile modFile() {
|
||||
return modFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
|
||||
+14
-15
@@ -31,10 +31,8 @@ public class ModValidator {
|
||||
private final List<ModFile> candidatePlugins;
|
||||
private final List<ModFile> candidateMods;
|
||||
private final List<ModFile> gameLibraries;
|
||||
private LoadingModList loadingModList;
|
||||
private final List<IModFile> brokenFiles = new ArrayList<>();
|
||||
private final List<EarlyLoadingException.ExceptionData> discoveryErrorData;
|
||||
private final List<ModFileLoadingException> modFileLoadingExceptions;
|
||||
|
||||
public ModValidator(Map<IModFile.Type, List<ModFile>> modFiles, List<IModFileInfo> brokenFiles, List<EarlyLoadingException.ExceptionData> discoveryErrorData, List<ModFileLoadingException> modFileLoadingExceptions) {
|
||||
this.candidateMods = lst(modFiles.get(IModFile.Type.MOD));
|
||||
@@ -44,7 +42,6 @@ public class ModValidator {
|
||||
this.candidatePlugins.addAll(lst(modFiles.get(IModFile.Type.LIBRARY)));
|
||||
this.discoveryErrorData = discoveryErrorData;
|
||||
this.brokenFiles.addAll(brokenFiles.stream().map(IModFileInfo::getFile).toList());
|
||||
this.modFileLoadingExceptions = modFileLoadingExceptions;
|
||||
}
|
||||
|
||||
public ModValidator(Map<IModFile.Type, List<ModFile>> modFiles, List<IModFileInfo> brokenFiles, List<EarlyLoadingException.ExceptionData> discoveryErrorData) {
|
||||
@@ -52,7 +49,9 @@ public class ModValidator {
|
||||
}
|
||||
|
||||
private static List<ModFile> lst(List<ModFile> files) {
|
||||
return files == null ? new ArrayList<>() : new ArrayList<>(files);
|
||||
if (files == null) return new ArrayList<>();
|
||||
if (files instanceof ArrayList) return files;
|
||||
return new ArrayList<>(files);
|
||||
}
|
||||
|
||||
public void stage1Validation() {
|
||||
@@ -60,11 +59,11 @@ public class ModValidator {
|
||||
if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) {
|
||||
LOGGER.debug(LogMarkers.SCAN, "Found {} mod files with {} mods", candidateMods.size(), candidateMods.stream().mapToInt(mf -> mf.getModInfos().size()).sum());
|
||||
}
|
||||
ImmediateWindowHandler.updateProgress("Found "+candidateMods.size()+" mod candidates");
|
||||
ImmediateWindowHandler.updateProgress("Found " + candidateMods.size() + " mod candidates");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<ModFile> validateFiles(final List<ModFile> mods) {
|
||||
private static List<ModFile> validateFiles(final List<ModFile> mods) {
|
||||
final List<ModFile> brokenFiles = new ArrayList<>();
|
||||
for (Iterator<ModFile> iterator = mods.iterator(); iterator.hasNext();) {
|
||||
ModFile modFile = iterator.next();
|
||||
@@ -87,7 +86,7 @@ public class ModValidator {
|
||||
// If any detectable error happens during the sorting process {missing deps, duplicates,
|
||||
// This helps prevent coremods/mixins from screwing up us displaying the error screens that the sorting/validation is trying to display.
|
||||
// This won't fix them all as they are still loaded and will still apply but it might help
|
||||
for (var info : this.loadingModList.getModFiles())
|
||||
for (var info : LoadingModList.getModFiles())
|
||||
mods.add(info.getFile().getSecureJar());
|
||||
|
||||
// Add any game libraries, this *may* be duplicates, depending on the state of the sorting. But until I get around to re-writing
|
||||
@@ -106,8 +105,8 @@ public class ModValidator {
|
||||
|
||||
private List<EarlyLoadingException.ExceptionData> validateLanguages() {
|
||||
List<EarlyLoadingException.ExceptionData> errorData = new ArrayList<>();
|
||||
for (Iterator<ModFile> iterator = this.candidateMods.iterator(); iterator.hasNext(); ) {
|
||||
final ModFile modFile = iterator.next();
|
||||
for (Iterator<ModFile> iterator = this.candidateMods.iterator(); iterator.hasNext();) {
|
||||
var modFile = iterator.next();
|
||||
try {
|
||||
modFile.identifyLanguage();
|
||||
} catch (EarlyLoadingException e) {
|
||||
@@ -124,12 +123,12 @@ public class ModValidator {
|
||||
var allErrors = new ArrayList<>(errors);
|
||||
allErrors.addAll(this.discoveryErrorData);
|
||||
|
||||
loadingModList = ModSorter.sort(candidateMods, allErrors);
|
||||
loadingModList.addCoreMods();
|
||||
loadingModList.addAccessTransformers();
|
||||
loadingModList.setBrokenFiles(brokenFiles);
|
||||
BackgroundScanHandler backgroundScanHandler = new BackgroundScanHandler(candidateMods);
|
||||
loadingModList.addForScanning(backgroundScanHandler);
|
||||
ModSorter.sort(candidateMods, allErrors);
|
||||
LoadingModList.getBrokenFiles().addAll(brokenFiles);
|
||||
var backgroundScanHandler = new BackgroundScanHandler(candidateMods);
|
||||
LoadingModList.getModFiles().stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.forEach(backgroundScanHandler::submitForScanning);
|
||||
return backgroundScanHandler;
|
||||
}
|
||||
}
|
||||
-1
@@ -17,7 +17,6 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
+34
-15
@@ -6,6 +6,8 @@
|
||||
package net.minecraftforge.fml.loading.moddiscovery;
|
||||
|
||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import net.minecraftforge.forgespi.language.IConfigurable;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
|
||||
@@ -16,11 +18,12 @@ import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
@ApiStatus.Internal
|
||||
class NightConfigWrapper implements IConfigurable {
|
||||
final class NightConfigWrapper implements IConfigurable {
|
||||
private final UnmodifiableConfig config;
|
||||
private IModFileInfo file;
|
||||
|
||||
@@ -28,35 +31,51 @@ class NightConfigWrapper implements IConfigurable {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
NightConfigWrapper setFile(IModFileInfo file) {
|
||||
private NightConfigWrapper(UnmodifiableConfig config, IModFileInfo file) {
|
||||
this.config = config;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
void setFile(IModFileInfo file) {
|
||||
this.file = file;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getConfigElement(String key) {
|
||||
var path = List.of(key);
|
||||
return Optional.ofNullable(validate(this.config.get(path), path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> getConfigElement(final String... key) {
|
||||
var path = asList(key);
|
||||
return this.config.getOptional(path).map(value -> {
|
||||
if (value instanceof UnmodifiableConfig) {
|
||||
return (T) ((UnmodifiableConfig) value).valueMap();
|
||||
} else if (value instanceof ArrayList<?> al && al.size() > 0 && al.get(0) instanceof UnmodifiableConfig) {
|
||||
throw new InvalidModFileException("The configuration path " + path + " is invalid. I wasn't expecting a multi-object list - remove one of the [[ ]]", file);
|
||||
}
|
||||
return (T) value;
|
||||
});
|
||||
return Optional.ofNullable(validate(this.config.get(path), path));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T validate(@Nullable T value, List<String> path) {
|
||||
if (value instanceof UnmodifiableConfig cfg) {
|
||||
// New Night config doesn't implement valueMap(), so do a copy.
|
||||
var entries = cfg.entrySet();
|
||||
var builder = ImmutableMap.builderWithExpectedSize(entries.size());
|
||||
for (var e : entries)
|
||||
builder.put(e.getKey(), e.getValue());
|
||||
return (T) builder.build();
|
||||
} else if (value instanceof ArrayList<?> al && !al.isEmpty() && al.getFirst() instanceof UnmodifiableConfig) {
|
||||
throw new InvalidModFileException("The configuration path " + path + " is invalid. I wasn't expecting a multi-object list - remove one of the [[ ]]", file);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends IConfigurable> getConfigList(final String... key) {
|
||||
final List<String> path = asList(key);
|
||||
if (this.config.contains(path) && !(this.config.get(path) instanceof Collection)) {
|
||||
throw new InvalidModFileException("The configuration path "+path+" is invalid. Expecting a collection!", file);
|
||||
throw new InvalidModFileException("The configuration path " + path + " is invalid. Expecting a collection!", file);
|
||||
}
|
||||
final Collection<UnmodifiableConfig> nestedConfigs = this.config.getOrElse(path, ArrayList::new);
|
||||
return nestedConfigs.stream()
|
||||
.map(NightConfigWrapper::new)
|
||||
.map(cw->cw.setFile(file))
|
||||
.map(conf -> new NightConfigWrapper(conf, file))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -21,37 +21,36 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
@ApiStatus.Internal
|
||||
class Scanner {
|
||||
record Scanner(ModFile fileToScan, ModFileScanData result) {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final ModFile fileToScan;
|
||||
private static final boolean DEBUG = LOGGER.isDebugEnabled(LogMarkers.SCAN);
|
||||
|
||||
public Scanner(final ModFile fileToScan) {
|
||||
this.fileToScan = fileToScan;
|
||||
public Scanner(ModFile fileToScan) {
|
||||
this(fileToScan, new ModFileScanData());
|
||||
}
|
||||
|
||||
public ModFileScanData scan() {
|
||||
ModFileScanData result = new ModFileScanData();
|
||||
result.addModFileInfo(fileToScan.getModFileInfo());
|
||||
fileToScan.scanFile(p -> fileVisitor(p, result));
|
||||
fileToScan.scanFile(this::fileVisitor);
|
||||
final List<IModLanguageProvider> loaders = fileToScan.getLoaders();
|
||||
if (loaders != null) {
|
||||
loaders.forEach(loader -> {
|
||||
LOGGER.debug(LogMarkers.SCAN, "Scanning {} with language loader {}", fileToScan.getFilePath(), loader.name());
|
||||
for (IModLanguageProvider loader : loaders) {
|
||||
if (DEBUG) LOGGER.debug("Scanning {} with language loader {}", fileToScan.getFilePath(), loader.name());
|
||||
loader.getFileVisitor().accept(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void fileVisitor(final Path path, final ModFileScanData result) {
|
||||
LOGGER.debug(LogMarkers.SCAN,"Scanning {} path {}", fileToScan, path);
|
||||
try (InputStream in = Files.newInputStream(path)){
|
||||
private void fileVisitor(final Path path) {
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
ModClassVisitor mcv = new ModClassVisitor();
|
||||
ClassReader cr = new ClassReader(in);
|
||||
cr.accept(mcv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
|
||||
mcv.buildData(result.getClasses(), result.getAnnotations());
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
// mark path bad
|
||||
if (DEBUG) LOGGER.warn("Failed scanning {} path {}", fileToScan, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -56,7 +56,7 @@ public class StartupNotificationManager {
|
||||
private synchronized static void addMessage(Message.MessageType type, String message, int maxSize)
|
||||
{
|
||||
EnumMap<Message.MessageType, List<Message>> newMessages = new EnumMap<>(messages);
|
||||
newMessages.compute(type, (key, existingList) -> {
|
||||
newMessages.compute(type, (_, existingList) -> {
|
||||
List<Message> newList = new ArrayList<>();
|
||||
if (existingList != null)
|
||||
{
|
||||
|
||||
@@ -24,8 +24,8 @@ import java.util.function.Supplier;
|
||||
@ApiStatus.Internal
|
||||
class ArgumentList {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private List<Supplier<String[]>> entries = new ArrayList<>();
|
||||
private Map<String, EntryValue> values = new HashMap<>();
|
||||
private final List<Supplier<String[]>> entries = new ArrayList<>();
|
||||
private final Map<String, EntryValue> values = new HashMap<>();
|
||||
|
||||
public static ArgumentList from(String... args) {
|
||||
ArgumentList ret = new ArgumentList();
|
||||
@@ -77,8 +77,12 @@ class ArgumentList {
|
||||
|
||||
public String[] getArguments() {
|
||||
return entries.stream()
|
||||
.flatMap(e -> Arrays.asList(e.get()).stream())
|
||||
.toArray(size -> new String[size]);
|
||||
.flatMap(e -> Arrays.stream(e.get()))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
public boolean has(String key) {
|
||||
return this.values.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean hasValue(String key) {
|
||||
@@ -122,7 +126,7 @@ class ArgumentList {
|
||||
return ent.getValue();
|
||||
}
|
||||
|
||||
private class EntryValue implements Supplier<String[]> {
|
||||
private static final class EntryValue implements Supplier<String[]> {
|
||||
private final String prefix;
|
||||
private final String key;
|
||||
private final boolean split;
|
||||
|
||||
+32
-7
@@ -22,6 +22,10 @@ abstract class CommonDevLaunchHandler extends CommonLaunchHandler {
|
||||
super(type, prefix);
|
||||
}
|
||||
|
||||
protected CommonDevLaunchHandler(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override public String getNaming() { return "mcp"; }
|
||||
@Override public boolean isProduction() { return false; }
|
||||
|
||||
@@ -29,18 +33,40 @@ abstract class CommonDevLaunchHandler extends CommonLaunchHandler {
|
||||
protected String[] preLaunch(String[] arguments, ModuleLayer layer) {
|
||||
super.preLaunch(arguments, layer);
|
||||
|
||||
var args = ArgumentList.from(arguments);
|
||||
|
||||
if (this.module == null) {
|
||||
var entry = args.get("launchEntry");
|
||||
if (entry == null)
|
||||
throw new IllegalArgumentException("When using " + this.name() + " launch target you must specify a --launchEntry argument");
|
||||
|
||||
args.remove("launchEntry");
|
||||
|
||||
int idx = entry.indexOf('/');
|
||||
if (idx == -1) {
|
||||
this.module = "minecraft";
|
||||
this.main = entry;
|
||||
} else {
|
||||
this.module = entry.substring(0, idx);
|
||||
this.main = entry.substring(idx + 1);
|
||||
}
|
||||
|
||||
if (args.has("launchData")) {
|
||||
args.remove("launchData");
|
||||
this.isData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (getDist().isDedicatedServer())
|
||||
return arguments;
|
||||
return args.getArguments();
|
||||
|
||||
if (isData())
|
||||
return arguments;
|
||||
|
||||
var args = ArgumentList.from(arguments);
|
||||
return args.getArguments();
|
||||
|
||||
String username = args.get("username");
|
||||
if (username != null) { // Replace '#' placeholders with random numbers
|
||||
Matcher m = Pattern.compile("#+").matcher(username);
|
||||
StringBuffer replaced = new StringBuffer();
|
||||
StringBuilder replaced = new StringBuilder();
|
||||
while (m.find()) {
|
||||
m.appendReplacement(replaced, getRandomNumbers(m.group().length()));
|
||||
}
|
||||
@@ -115,7 +141,7 @@ abstract class CommonDevLaunchHandler extends CommonLaunchHandler {
|
||||
protected static Path getForgeOnly(Path forge) {
|
||||
var packages = getPackages(); // Pulled out so it is passed to the lambda as value
|
||||
// We need to separate out our resources/code so that we can show up as a different data pack.
|
||||
var modJar = SecureJar.from((path, base) -> {
|
||||
var modJar = SecureJar.from((path, _) -> {
|
||||
if (!path.endsWith(".class")) return true;
|
||||
for (var pkg : packages)
|
||||
if (path.startsWith(pkg)) return true;
|
||||
@@ -130,7 +156,6 @@ abstract class CommonDevLaunchHandler extends CommonLaunchHandler {
|
||||
return new String[] {
|
||||
"net/minecraftforge/",
|
||||
"META-INF/services/",
|
||||
"META-INF/coremods.json",
|
||||
"META-INF/mods.toml"
|
||||
};
|
||||
}
|
||||
|
||||
Loaded 100 of 2759 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user