Compare commits

..
57 Commits
Author SHA1 Message Date
berkay8987 beb5ccc378 Fix missing forge config UI translation keys (#10914) 2026-11-02 18:00:01 -08:00
LexManos 8199135df7 Add protection against large network channel lists 2026-10-14 20:13:33 -07:00
LexManos 32359d7e76 Fix legacy object holders not being fired in the registry specific codepath
Better optimize the scanning for object holders by unrolling streams.
2026-10-13 10:03:37 -07:00
BonsUnleashedandLexManos abf2889b11 Store object holder handlers per registry name (#10899)
Co-authored-by: LexManos <[email protected]>
2026-10-12 17:29:50 -07:00
LexManos 82f7d4311e Fix generic errors in new Eclipse compiler 2026-10-12 17:29:31 -07:00
Meme Man a11d1936c5 Update equality check for RegistryObject to include RegistryObject#key. (#10857) 2026-07-21 11:03:00 -07:00
Matt 75f340f2a4 Toml parsing errors are now visible in the latest log (#10843) 2026-07-09 13:10:52 -07:00
Matt 8c50fdebc1 Add support for multiple access transformer configs. 2026-03-23 11:26:18 -07:00
Jonathing 0675635b0c Migrate the MDK to ForgeGradle 7
Backport of d29524b to 1.21.1
2026-03-18 23:12:37 +00:00
Paint_Ninja 96a894d3ec Skip processing vanilla classes in RuntimeDistCleaner when running on a client
Backport of #10682 to 1.21.1
2026-03-17 21:00:46 +00:00
Paint_Ninja e1461a3185 Bump ASM and EventBus
- ASM 9.9.1 for Java 26 support
- EventBus 6.2.33 for Event#cancel() convenience method and further performance improvements
2026-03-17 20:59:24 +00:00
LexManos cc24b60942 Update SecureModules to address ZipPath bug with brackets. Closes #10755 2026-01-20 18:53:58 -08:00
MattandLexManos 879a5d3245 Optimize memory allocation for checking entity fluid collisions. Backport of #10745 (#10752)
Co-authored-by: LexManos <[email protected]>
2026-01-16 09:26:52 -08:00
LexManos 0cf398c509 Mark applyServerBinpatches as a dependency of BundleList. Fixes: #10729 2025-12-04 09:21:57 -08:00
LexManos d2f441ed41 Migrate to Github Actions 2025-12-02 14:58:40 -08:00
Matt 444d4a94da Fix incorrect blend state for blur effects, followup to #10115 and fix for 10541 (#10678) 2025-10-20 17:39:13 -07:00
MattandLexManos 86a00f6122 Fix crafter not inserting into iitemhandler (#10667) (#10669)
Co-authored-by: LexManos <[email protected]>
2025-09-25 13:46:56 -07:00
Paint_Ninja fe8684891c Update ASM version from 9.7.1 to 9.8 2025-09-22 11:55:10 +01:00
Joseph T. McQuigg e2a81d278c Allow command suggestions/autocomplete to search all modded paths at once without namespace provided (#10594) 2025-08-03 09:44:44 -07:00
Matt 86e2a11c8e Add ForgeLayeredDraw & AddGuiOverlayLayersEvent as API for gui overlay management. (#10583) 2025-07-27 10:24:57 -07:00
Joseph T. McQuigg 927edb8533 Add Pumpkin Block and Item Tags (#10511)
Backport of #10482 to 1.21.1
2025-04-19 13:29:57 +01:00
Jonathing 0bca349d64 1.21.1 RB 1
https://forums.minecraftforge.net/topic/154391-forge-521-minecraft-1211/
2025-03-04 15:49:44 -05:00
Jonathing 7a44626d5e Add #forge:chorus_additionally_grows_on tag for similiar mechanics to #minecraft:azalea_grows_on but for chorus (#10461) 2025-03-04 12:39:44 -08:00
JonathingandLexManos 30ec79b19e Speed up mod annotation scanning by ~30% (#10467)
Co-authored-by: LexManos <[email protected]>
2025-03-04 12:27:17 -08:00
Jonathing 0794e51f62 Add missed license headers (#10476) 2025-03-04 12:26:57 -08:00
JonathingandLexManos c86172db31 Redstone updates and gametests structure fixes (#10422)
Fix game test structure rotations being inconsistent (#10391)
Fix redstone update order not matching vanilla. (#10392)
Fix redstone updates for comparators going up/down incorrectly. Related: #9973
Make block pos assertions in GameTestHelper behave like in 1.21.3, thus fixing rotated tests functionality.
This can be disabled in the Forge config if you have old tests that this breaks. Tho it is recommended you update your tests.

---------
Co-authored-by: LexManos <[email protected]>
2025-03-04 12:26:42 -08:00
Jonathing ee14c0c911 Bump dependencies (#10386)
- CoreMods 5.2.4 -> 5.2.6
- EventBus 6.2.15 -> 6.2.27
    - Reduced memory usage
    - Further performance improvements
    - Fixed EventBus#39, unregistered listeners are now immediately reclaimable by the GC
- Apache MavenArtifact 3.8.5 -> 3.8.8
- NightConfig 3.7.3 -> 3.7.4
2025-02-17 18:46:48 -08:00
b3a4ac864f Re-introduce ItemStack Capabilities (#10363)
Co-authored-by: RealMangoRage <[email protected]>
Co-authored-by: LexManos <[email protected]>
2025-02-17 18:40:35 -08:00
Jonathing f9139a3002 Re-introduce IForgeItem.damageItem when an item takes damage, Fixes #10344 (#10373) 2025-02-17 18:35:26 -08:00
Jonathing 784693b7e5 Add updated tag context to AddReloadListenerEvent (#10421)
Fixes using custom tags in GlobalLootModifiers.
2025-02-17 18:28:07 -08:00
Jonathing 774048154f Add fast graphics render type to block model jsons (#10396)
Make modded leaves behave like vanilla leaves by default (Fixes #10389)
2025-02-17 18:22:26 -08:00
Jonathing 490e35ba8e Fix corrupted and invalidly symlinked worlds crashing on level select (#10420) 2025-02-17 18:18:10 -08:00
Jonathing 667a164430 Honor attacker shield disabling status (#10318) 2025-01-27 15:33:47 -08:00
Joseph T. McQuigg 0434d67016 Fix issues in VillagerTrades.EmeraldsForVillagerTypeItem related to custom Villager Types (#10300)
Co-authored-by: Joseph T. McQuigg <[email protected]>
2025-01-27 15:33:29 -08:00
JonathingandLexManos 02229557ed GameTestHelper additions (#10340)
Co-authored-by: LexManos <[email protected]>
2025-01-27 09:58:00 -08:00
Jonathing 79224a70e6 Update Gradle to 8.12.1 (#10354)
Automate IntelliJ download sources in MDK
2025-01-27 09:57:46 -08:00
Jonathing 5c9c8ce539 Fix JOpt Simple by bumping ModLauncher (#10349) 2025-01-27 09:57:09 -08:00
Jonathing c186c38113 Fix JOpt Simple needing a strict version requirement declaration (#10313) 2025-01-25 18:39:10 +00:00
Jonathing 1507717fae Fix incorrect method reference in TntBlock.explode() (#10328) 2025-01-25 18:38:43 +00:00
Jonathing 12c4233fac Bump dependencies (#10276)
- EventBus 6.2.8 -> 6.2.15
- Bootstrap 2.1.3 -> 2.1.8
- SecureModules 2.2.20 -> 2.2.21
- AccessTransformers 8.2.0 -> 8.2.1
- ModLauncher 10.2.2 -> 10.2.3
These bumps include further cleanup and optimisations.
2025-01-04 16:42:08 +00:00
Jonathing f331376d9d Simplify Title Screen Brandings (#10289) 2025-01-02 21:57:02 -08:00
Gabriel Harris-Rouquette b5b5064a4f Update jline for better darwin/arm64 support 2024-12-30 09:47:02 -08:00
JonathingandPaint_Ninja f4801960e7 Skip Vanilla classes for the CapabilityTokenSubclass transformer (#10224)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 23:10:40 -08:00
Jonathing 68c60c17e5 Bump CoreMods to 5.2.4 (#10266) 2024-12-23 15:47:22 -08:00
JonathingandPaint_Ninja 4a5f977691 Skip processing Forge classes in RuntimeDistCleaner (#10211)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 14:03:50 -08:00
JonathingandPaint_Ninja 896cfb38a3 Skip Forge classes in the RuntimeEnumExtender transformer (#10197) (#10219)
Mod classes are still transformed as usual

Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 13:50:21 -08:00
JonathingandPaint_Ninja 185273022c Deprecate @ObjectHolder, add a couple of fast-paths (#10195) (#10231)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 13:40:29 -08:00
Jonathing eb65e1db51 Allow mipmap lowering to be disabled (#10255) 2024-12-23 13:30:30 -08:00
JonathingandPaint_Ninja 6a5c413a13 Simplify memory usage display on loading screen (#10236)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 13:30:01 -08:00
Jonathing 1b7b2b6ff1 Fix NightConfig FileWatcher causing dedicated server to hang when exiting (#10215) 2024-12-23 13:03:27 -08:00
Martijn van den Brand cb290f23f5 Pass player argument when firing OnDatapackSyncEvent (#10171) 2024-11-16 21:34:03 -08:00
Jonathing 6e8eaeeec9 Add optional fix of use item duration, disabled by default (#10151) 2024-11-10 13:17:08 -08:00
Jonathing 56c41f3ca8 Bump CoreMods to 5.2 (#10160)
Full Changelog: https://gist.github.com/Jonathing/c3ad28b2a048ac839a7baba5417ee870

The key features are:
- ES6 language support
- Thoroughly updated ASMAPI, with full documentation
- Bug fixes (some optional for backwards-compatibility)
- Partial internal code cleanup
- Request CoreMods to not apply fix for ASMAPI.findFirstInstructionBefore by default
2024-11-08 12:41:02 -08:00
LexManos 0084b8cf48 Add proper support for JPMS in normal mods.toml mods (#10125)
Make FMLModContainer read and apply Add-Opens and Add-Exports manifest entries from mod files.
Forge is now the `net.minecraftforge.forge` module instead of `forge`

https://forums.minecraftforge.net/topic/153333-proper-java-module-support-in-forge-mods/
2024-11-04 17:11:56 -08:00
Paint_Ninja eb6c564ce4 Add more c fluid tags (#10137)
Replaces the existing `forge`-namespaced tags, which are now deprecated.
Adds `c:experience`
2024-11-04 18:21:54 +00:00
Paint_Ninja 618da273dc Add c:stripped_logs and c:stripped_woods tags (#10136) 2024-10-26 19:47:54 +01:00
Jonathing 074c0cdfd6 [1.21.1] Bump CoreMods and ASM (#10127)
CoreMods 5.1.13 changelog: https://gist.github.com/Jonathing/905d142447ecef670526ae27243adbba
2024-10-26 11:45:37 +01:00
1865 changed files with 42219 additions and 32390 deletions

No files matched your search

-1
View File
@@ -3,7 +3,6 @@
*.patch text eol=lf
*.java text eol=lf
*.gradle text eol=crlf
**/MANIFEST.MF text eol=crlf
*.png binary
*.gif binary
*.exe binary
+1 -1
View File
@@ -10,4 +10,4 @@ assignees: ''
<!-- Please note, feature requests that are submitted as GitHub issues here are rarely implemented. -->
<!-- If you'd like to see something in Forge, you should be ready to implement it as a PR. -->
<!-- If you're not sure, open a Draft PR or discuss it in the Discord server. -->
<!-- Discord: https://discord..minecraftforge.net/ -->
<!-- Discord: https://discord.gg/UvedJ9m -->
-1
View File
@@ -11,7 +11,6 @@ assignees: ''
<!-- For support with crashes and issues, please create a post on the forum instead: -->
<!-- https://www.minecraftforge.net/forum/forum/18-support-bug-reports/ -->
<!-- You can also use the discord for support, https://discord.minecraftforge.net/ -->
**Minecraft Version:** {Minecraft version}
+1 -1
View File
@@ -2,7 +2,7 @@ name: Publish
on:
push:
branches: [ '1.21.11' ]
branches: [ '1.21.1' ]
permissions:
contents: read
+15 -19
View File
@@ -1,6 +1,3 @@
#forgedev dev if the folder exists in here
forgedev
#eclipse
**/bin
**/.settings
@@ -25,12 +22,11 @@ forgedev
/*/build
/*/.gradle
# Minecraft
/run
/*.launch
# Projects repo, either ignore it, or ignore patches.
src/minecraft
/projects/mcp/
/projects/clean/
/projects/forge/
#/patches/*
#occupational hazards
/projects/**/build/
@@ -39,20 +35,20 @@ src/minecraft
/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/
/lib/
/_actual/
/runs/
/forge.ipr
/forge.iws
+1 -1
View File
@@ -3,7 +3,7 @@ parts herein are licensed under the terms of the LGPL 2.1 found
here http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt and
copied below.
Homepage: https://minecraftforge.net/
Homepage: http://minecraftforge.net/
https://github.com/MinecraftForge/MinecraftForge
+36 -857
View File
@@ -1,876 +1,55 @@
import org.apache.tools.ant.filters.ReplaceTokens
import net.minecraftforge.forge.tasks.*
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'idea'
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 'eclipse'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.versions
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
alias libs.plugins.renamer
alias libs.plugins.jarsigner
id 'net.minecraftforge.forgedev'
id 'net.minecraftforge.forge.build.convention'
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.gradlejarsigner' version '1.0.4'
id 'org.barfuin.gradle.taskinfo' version '2.1.0'
}
gradleutils.displayName = 'Forge'
group = 'net.minecraftforge'
description = 'Modifications to Minecraft to enable mod developers.'
Util.init() //Init all our extension methods!
// NOTE: See settings.gradle for all the referenceable version variables.
// All subprojects (that apply 'net.minecraftforge.forge.build.convention') will have these available.
println "Version: $version"
java.toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
jarSigner.autoDetect('forge')
// We have to tell ForgeDev to use the exact versions of tools we ship in the installer
fdtools {
configure('binpatcher') {
version = buildLibs.binarypatcher.get().version
}
configure('installertools') {
version = buildLibs.installertools.get().version
}
configure('jarcompatibilitychecker') {
javaLauncher = javaToolchains.launcherFor(java.toolchain)
}
}
renamerTools {
configure('classes') {
artifact = buildLibs.renamer.get().toString() + ':all'
}
configure('srg2source') {
version = buildLibs.srg2source.get().version
javaLauncher = javaToolchains.launcherFor(java.toolchain)
}
}
/* This is used for debugging, comparing all published artifacts against actual production
forgedev.validatePublish {
task {
baseBranch = 'upstream/1.21.11'
versionPrefix = minecraftVersion
}
}
*/
final minecraftFiles = forgedev.minecraftFiles(minecraftVersion)
final mcpBase = forgedev.mcpBase {
mcpVersion = "${minecraftVersion}-${project.ext.mcpVersion}"
mappingChannel = mappingsChannel
mappingVersion = mappingsVersion
accessTransformers.from file('src/main/resources/META-INF/accesstransformer.cfg')
sideAnnotationStrippers.from file('src/main/resources/forge.sas')
}
forgedev.patches {
base = mcpBase
patches = file('patches/minecraft')
patched = file('src/minecraft/java')
sourceSets.main.java.srcDir patched
}
tasks.register('setup') {
dependsOn forgedev.patches.apply
}
sourceSets {
named('main') {
resources.srcDir 'src/main/generated'
}
named('test') {
resources.srcDir 'src/test/generated'
}
}
license {
header = file('LICENSE-header.txt')
include 'net/minecraftforge/'
exclude 'net/minecraftforge/common/LenientUnboundedMapCodec.java'
tasks.tap {
register('main') {
files.from files('src/main/java')
}
register('test') {
files.from files('src/test/java')
}
}
ext {
GIT_INFO = gradleutils.gitInfo
VERSION = gradleutils.getFilteredMCTagOffsetBranchVersion(true, '[0-9]', MC_VERSION)
FORGE_VERSION = VERSION.substring(MC_VERSION.length() + 1)
}
changelog {
from changelogBase
publishAll = false
from '51.0' // Since 1.21
}
// TODO [ForgeDev] Support compileOnlyApi, runtimeOnly, and a consumable annotation processor configuration
configurations {
// Don't pull all libraries, if we're missing something, add it to the installer list so the installer knows to download it.
register('bootstrap') {
transitive = false
canBeDeclared = true
canBeResolved = true
}
tasks.register('setup') {
dependsOn ':forge:extractMapped'
if (findProject(':clean'))
dependsOn ':clean:extractMapped'
}
register('installer') {
transitive = false
canBeDeclared = true
canBeResolved = true
extendsFrom(bootstrap)
}
tasks.register('doChecks') {
dependsOn ':forge:checkJarCompatibility'
dependsOn ':forge:publish'
}
register('installerextra') {
transitive = false
extendsFrom(bootstrap)
}
named('api') {
extendsFrom(installer)
}
named('implementation') {
// Inherit vanilla Minecraft's dependencies
extendsFrom(mcpBase.dependencyConfiguration)
project(':mcp') {
apply plugin: 'net.minecraftforge.gradle.mcp'
mcp {
config MC_VERSION + '-' + MCP_VERSION
pipeline = 'joined'
}
}
dependencies {
// These need to actually be on the classpath at the start. This is only used for the server shim jar.
// 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.jspecify // Dep of EventBus
installer libs.forgespi
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 projects.fmlcore
installer projects.fmlloader
installer projects.fmlearlydisplay
installer projects.javafmllanguage
installer projects.lowcodelanguage
installer projects.mclanguage
installer projects.forgeTransformers
// Remove when we bump to a version with no srg names, as this is needed to map srg->official at runtimes
installerextra("de.oceanlabs.mcp:mcp_config:${mcpBase.mcpVersion.get().toString()}:srg2off")
// Get the zip file with csv's of srg->mapepd for Runtime SRG->Mapped reflection remapping
// This is also only needed during dev time, and automatically added to the dep list by Mavenizer for userdev
// We shouldn't need this anymore now that we use runtime official mappings, but some of our code still references SRG names via reflection at dev time
runtimeOnly files(mcpBase.mappingZip)
runtimeOnly files(mcpBase.extra)
runtimeOnly libs.bootstrap
runtimeOnly libs.bootstrap.dev
annotationProcessor libs.eventbus.validator
}
final EXTRA_TXTS = [
file('LICENSE.txt'),
changelog.task.flatMap(task -> task.outputFile)
]
forgedev.runs {
configureEach {
options {
workingDir = layout.projectDirectory.dir("runs/$name")
mainClass = 'net.minecraftforge.bootstrap.ForgeBootstrap'
args '--gameDir', '.'
jvmArgs '-Djava.net.preferIPv6Addresses=system'
systemProperty 'bsl.debug', 'true'
systemProperty 'terminal.jline', 'true'
with(sourceSets.test) {
systemProperty 'forge.enableGameTest', 'true'
systemProperty 'forgedev.enableTestMods', 'true'
}
}
}
create('client') {
options {
systemProperty 'eventbus.api.strictRuntimeChecks', 'true'
systemProperty 'org.lwjgl.system.SharedLibraryExtractDirectory', 'lwjgl_dll'
args '--launchTarget', 'forge_dev_client',
'--username', 'Dev',
'--version', project.name,
'--accessToken', '0',
'--userType', 'mojang',
'--versionType', 'release',
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
}
}
create('server') {
options {
args '--launchTarget', 'forge_dev_server'
}
}
create('gameTestServer') {
options {
args '--launchTarget', 'forge_dev_server_gametest'
args '--uniqueWorld' // Unique world is used so that the world regenerates, as well as the world config isn't influenced by other runs
}
}
create('data') {
options {
args '--launchTarget', 'forge_dev_data',
'--all',
'--existing', sourceSets.main.resources.srcDirs[0],
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
with(sourceSets.main) {
args '--mod', 'forge',
'--output', rootProject.file('src/main/generated/')
}
with(sourceSets.test) {
args '--mod', '.+',
'--existing', sourceSets.test.resources.srcDirs[0],
'--output', rootProject.file('src/test/generated/')
}
}
}
create('clientData') {
options {
args '--all',
'--existing', sourceSets.main.resources.srcDirs[0],
'--assetsDir', '{assets_root}',
'--assetIndex', '{asset_index}'
with(sourceSets.main) {
args '--launchTarget', 'forge_dev_client_data',
'--mod', 'forge',
'--output', rootProject.file('src/main/generated/'),
'--mod', 'forge'
}
with(sourceSets.test) {
args '--launchTarget', 'forge_dev',
'--launchEntry', 'minecraft/net.minecraft.client.data.Main',
'--launchData',
'--mod', '.+',
'--existing', sourceSets.test.resources.srcDirs[0],
'--output', rootProject.file('src/test/generated/')
}
if (System.env.TEAMCITY_VERSION) {
//Only setup the CI environment if and only if the environment variables are set.
tasks.named('configureTeamCity').configure {
doLast {
println "##teamcity[buildNumber '${project(':forge').version}']"
println "##teamcity[setParameter name='env.PUBLISHED_JAVA_ARTIFACT_VERSION' value='${project(':forge').version}']"
}
}
}
tasks.register('genAllData') {
dependsOn forgedev.runs.data.run, forgedev.runs.data.runTest, forgedev.runs.clientData.run, forgedev.runs.clientData.runTest
}
final serverShim = forgedev.shim {
config {
mainClass = 'net.minecraftforge.bootstrap.ForgeBootstrap'
args = ['--launchTarget', 'forge_server']
}
classpath {
libraries configurations.installer
libraries configurations.installerextra
library tasks.named('universalJar', Jar)
library forgedev.binaryPatches('server').apply, 'server'
serverBundle.fileProvider(minecraftFiles.server)
}
bootstrapClasspath(configurations.bootstrap)
jar {
jarSigner.sign(it)
}
}
final finalizeSpawn = forgedev.methodCallFinder('findFinalizeSpawnTargets') {
jar = tasks.named('renameClientOfficial').flatMap{ it.output } //.fileProvider(mcpBase.classes)
output = rootProject.file('forge-transformers/src/main/resources/coremods/finalize_spawn_targets.json')
blacklist 'net/minecraft/world/level/BaseSpawner' // Ignore this class as we special case it.
invokeVirtual('finalizeSpawn', '(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/EntitySpawnReason;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;')
}
forgedev.validateDeprecations(tasks.named('jar', Jar)) {
mcVersion = minecraftVersion
}
forgedev.checks() {
base = mcpBase
ats {
mappings.fileProvider(mcpBase.map2Srg)
}
sas {}
execs { }
patches {
dependsOn forgedev.patches.make
patchDir = forgedev.patches.patches
patchesWithS2SArtifact = [
'minecraft/net/minecraft/client/renderer/ViewArea.java.patch',
'minecraft/net/minecraft/data/models/blockstates/Variant.java.patch',
]
}
fix {
dependsOn finalizeSpawn
}
forgedev.patches.make.configure {
finalizedBy forgedev.checks.patches.fix
}
}
final crowdin = forgedev.crowdin // Create the crowdin tasks
tasks.named('jar', Jar) {
from(EXTRA_TXTS)
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
exclude '.cache'
from(zipTree(crowdin.flatMap { it.output } )) {
include 'assets/forge/lang/*.json'
}
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.forge'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : gradleutils.vendor.get(),
'Specification-Version' : gitversion.info.tag,
'Implementation-Title' : project.group,
'Implementation-Vendor' : gradleutils.vendor.get(),
'Implementation-Version': forgeVersion
], 'net/minecraftforge/versions/forge/')
attributes([
'Specification-Title' : 'Minecraft',
'Specification-Vendor' : gradleutils.vendor.get(),
'Specification-Version' : minecraftVersion,
'Implementation-Title' : 'MCP',
'Implementation-Vendor' : gradleutils.vendor.get(),
'Implementation-Version': mcpVersion
], 'net/minecraftforge/versions/mcp/')
}
jarSigner.sign(it)
}
final universalJar = tasks.register('universalJar', Jar) {
from (zipTree(tasks.named('jar', Jar).flatMap { it.archiveFile } )) {
exclude(forgedev.filterVanilla(mcpBase.classes))
}
manifest = tasks.named('jar', Jar).get().manifest
archiveClassifier = 'universal'
jarSigner.sign(it)
}
final map2Tsrg = renamer.convert('map2Tsrg', mcpBase.map2Srg) // Remove when we bump renamer to version that supports gziped files (on MC version change)
final reobfJar = renamer.classes('srgJar', tasks.named('jar', Jar)) {
libraries.setFrom(configurations.compileClasspath)
mappings map2Tsrg
archiveClassifier = 'srg'
}
final universalJarSrg = tasks.register('universalJarSrg', Jar) {
from (zipTree(reobfJar.flatMap { it.output } )) {
exclude(forgedev.filterVanilla(mcpBase.classes))
}
manifest = universalJar.get().manifest
archiveClassifier = 'universal-srg'
jarSigner.sign(it)
}
final sourcesSrg = renamer.sources(sourceSets.main) {
apply {
archiveClassifier = 'sources-srg'
mappings mcpBase.map2Srg
excFiles.from files('src/main/resources/forge.exc')
}
extract {
mustRunAfter(forgedev.patches.apply)
}
}
final sourcesJar = tasks.register('sourcesJar', Jar) {
archiveClassifier = 'sources'
// If we don't need to reobf we can just do this
//from(sourceSets.main.allJava.srcDirs - forgedev.patches.patched.asFile.get())
dependsOn(sourcesSrg.apply)
from(zipTree(sourcesSrg.apply.flatMap{ it.output })) {
exclude { forgedev.patches.patched.file(it.path).get().asFile.exists() }
}
from(forgedev.userDev.patches.flatMap{ it.output }) {
into 'patches/'
}
}
forgedev.userDev {
base(mcpBase)
config {
universal universalJarSrg
sources sourcesJar
addLibraries(configurations.installer)
// 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)
// TODO [ForgeDev] Consolidate Patcher + UserDev Config runs in a sane way
runs {
configureEach {
parents = []
main = 'net.minecraftforge.bootstrap.ForgeBootstrap'
args '--gameDir', '.'
jvmArgs '-Djava.net.preferIPv6Addresses=system'
client = it.name.containsIgnoreCase('client')
environment 'MCP_MAPPINGS', '{mcp_mappings}'
}
register('clientData') {
args '--launchTarget', "forge_userdev_client_data"
args '--assetIndex', '{asset_index}'
args '--assetsDir', '{assets_root}'
}
register('client') {
property 'forge.enableGameTest', 'true'
args '--launchTarget', "forge_userdev_client"
args '--version', 'MOD_DEV'
args '--assetIndex', '{asset_index}'
args '--assetsDir', '{assets_root}'
}
register('server') {
property 'forge.enableGameTest', 'true'
args '--launchTarget', "forge_userdev_server"
}
register('gameTestServer') {
args '--launchTarget', "forge_userdev_server_gametest"
}
register('data') {
args '--launchTarget', "forge_userdev_data"
args '--assetIndex', '{asset_index}'
args '--assetsDir', '{assets_root}'
}
}
}
patches {
modified.setFrom(sourcesSrg.apply.flatMap{ it.output })
}
binaryPatches {
patches.setFrom(forgedev.patches.apply.flatMap { it.output })
dirty.setFrom(reobfJar.flatMap { it.output })
}
}
forgedev.userdevCompatibility(minecraftVersion) {
clean = mcpBase.classesRaw
dirty = reobfJar
}
// region Installer tasks, Not needed when we simplify the installer/remove obf
final renameOfficial = (side, inputProvider, mappingsProvider) -> {
renamer.classes('rename' + side + 'Official') {
additionalArgs = ['--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse']
input.fileProvider(inputProvider)
libraries = mcpBase.dependencyConfiguration
mappings mappingsProvider
output = layout.buildDirectory.file("libs/rename-${side.toLowerCase()}-official.jar")
logFile = layout.buildDirectory.file("libs/rename-${side.toLowerCase()}-official.log")
}
}
final clientOfficial = renameOfficial('Client', minecraftFiles.client, minecraftFiles.clientMappings)
final serverOfficial = renameOfficial('Server', minecraftFiles.serverExtracted, minecraftFiles.serverMappings)
final clientBinPatches = forgedev.binaryPatches('client') {
clean = clientOfficial.flatMap { it.output }
}
final serverBinPatches = forgedev.binaryPatches('server') {
clean = serverOfficial.flatMap { it.output }
}
final sanitizedClientMappings = renamer.convert("cleanClientMappings", minecraftFiles.clientMappings)
final sanitizedServerMappings = renamer.convert("cleanServerMappings", minecraftFiles.serverMappings)
// endregion
forgedev.installer {
dev = !forgedev.ci
offline = false
baseVersion = buildLibs.installer.get().version.toString()
// Add extra libraries
pack serverShim.jar // The Server executable jar IS needed to be packed, as the installer would need a spec bump in order to download it
pack universalJar // I don't think this is strictly needed to be packed anymore, will need to double check later
// This isn't actually needed, but its here for some reason, probably wasn't deleted when then installer process was cleaned up
library mcpBase.mcpArtifact.get()
launcherLibrary universalJar
// We configure the launcher here to keep the order for easier diff, after publishing, move this to the LauncherJson configuration below
launcherJson {
// We create this during install, so it's not downloadable
generated(clientBinPatches.apply, 'client')
}
launcherLibraries configurations.installerextra
launcherLibraries configurations.installer
final launcherId = "${minecraftVersion}-${project.name}-${forgeVersion}"
final installertools = tool(buildLibs.installertools)
final binarypatcher = tool(buildLibs.binarypatcher)
final renamer = tool(buildLibs.renamer)
jar {
from(EXTRA_TXTS)
from(rootProject.file('/forge_installer_logo.png')) {
rename { 'big_logo.png' }
}
from(clientBinPatches.create.flatMap { t -> t.output }) {
rename { 'data/client.lzma' }
}
from(serverBinPatches.create.flatMap { t -> t.output }) {
rename { 'data/server.lzma' }
}
final argsFile = rootProject.file('server_files/args.txt')
final tokens = [tokens: [
SHIM_JAR_FILE: serverShim.jar.get().archiveFileName.get(),
MAVEN_PATH: forgedev.mavenArtifact.directory
]]
from(argsFile) {
filter(tokens, ReplaceTokens)
into 'data'
rename { 'unix_args.txt' }
}
from(argsFile) {
filter(tokens, ReplaceTokens)
into 'data'
rename { 'win_args.txt' }
}
from(rootProject.file('server_files')) {
filter(tokens, ReplaceTokens)
into 'data'
exclude 'args.txt'
}
jarSigner.sign(it)
}
json {
icon = rootProject.file('icon.ico')
executable serverShim.jar
minecraft = minecraftVersion
profileVersion = launcherId
final projectArtifact = "${project.group}:${project.name}:${project.version}"
data 'MOJMAPS', "[net.minecraft:client:${minecraftVersion}:mappings@tsrg]", "[net.minecraft:server:${minecraftVersion}:mappings@tsrg]"
data 'MOJMAPS_SHA', sanitizedClientMappings.flatMap{ it.output }, sanitizedServerMappings.flatMap{ it.output }
data 'MC_UNPACKED', "[net.minecraft:client:${minecraftVersion}]", "[net.minecraft:server:${minecraftVersion}:unpacked]"
data 'MC_UNPACKED_SHA', minecraftFiles.client, minecraftFiles.serverExtracted
data 'MC_OFF', "[net.minecraft:client:${minecraftVersion}:official]", "[net.minecraft:server:${minecraftVersion}:official]"
data 'MC_OFF_SHA', clientOfficial.flatMap { it.output }, serverOfficial.flatMap { it.output }
data 'BINPATCH', '/data/client.lzma', '/data/server.lzma'
data 'PATCHED', "[$projectArtifact:client]", "[$projectArtifact:server]"
data 'PATCHED_SHA', clientBinPatches.apply.flatMap { it.output }, serverBinPatches.apply.flatMap { it.output }
def rename = (side, input) -> {
step(renamer) {
sides = [side]
args = [
'--input', input,
'--output', '{MC_OFF}',
'--names', '{MOJMAPS}',
'--ann-fix', '--ids-fix', '--src-fix', '--record-fix', '--strip-sigs', '--reverse'
]
cache '{MC_OFF}', '{MC_OFF_SHA}'
}
}
steps = [
extract(installertools) {
side = 'server'
archive '{INSTALLER}'
extract 'data/README.txt', '{ROOT}/README.txt'
extract 'data/run.sh', '{ROOT}/run.sh'
executable '{ROOT}/run.sh'
extract 'data/run.bat', '{ROOT}/run.bat'
optional 'data/user_jvm_args.txt', '{ROOT}/user_jvm_args.txt'
extract 'data/unix_args.txt', "{ROOT}/libraries/${forgedev.mavenArtifact.directory}/unix_args.txt"
extract 'data/win_args.txt', "{ROOT}/libraries/${forgedev.mavenArtifact.directory}/win_args.txt"
},
extractBundle(installertools) {
side = 'server'
libraries()
},
extractBundle(installertools) {
side = 'server'
jar '{MINECRAFT_JAR}', '{MC_UNPACKED}'
cache '{MC_UNPACKED}', '{MC_UNPACKED_SHA}'
},
step(installertools) {
args = [
'--task', 'DOWNLOAD_MOJMAPS',
'--sanitize',
'--version', minecraftVersion,
'--side', '{SIDE}',
'--output', '{MOJMAPS}'
]
cache '{MOJMAPS}', '{MOJMAPS_SHA}'
},
rename.call('server', '{MC_UNPACKED}'),
rename.call('client', '{MINECRAFT_JAR}'),
step(binarypatcher) {
args = [
'--clean', '{MC_OFF}',
'--output', '{PATCHED}',
'--apply', '{BINPATCH}',
'--data', '--unpatched'
]
cache '{PATCHED}', '{PATCHED_SHA}'
}
]
}
launcherJson {
// These are flags to preserve glitchy old behavior to make diffing easier, removing when migrating to production is recommended
sortLibraries = false // Don't sort, makes things non-deterministic, but easier to diff old versions
duplicateLibraries = true // Our 'bootstrap' layer go added twice
librariesLast = false // Makes libraries be added before args
id = launcherId
inheritsFrom = minecraftVersion
mainClass = 'net.minecraftforge.bootstrap.ForgeBootstrap'
gameArgs = [ '--launchTarget', 'forge_client' ]
jvmArgs = [ '-Djava.net.preferIPv6Addresses=system' ]
}
}
final mdkGradleWrapper = tasks.register('mdkGradleWrapper', Wrapper) {
gradleVersion = '9.5.0'
scriptFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradlew")
jarFile = layout.buildDirectory.dir("wrapper/" + gradleVersion + "/gradle/wrapper/gradle-wrapper.jar")
}
final mdkZip = tasks.register('mdkZip', Zip) {
dependsOn mdkGradleWrapper
archiveBaseName = project.name
archiveClassifier = 'mdk'
archiveVersion = project.version
destinationDirectory = file('build/libs')
from(EXTRA_TXTS)
from mdkGradleWrapper.map(Wrapper.&getScriptFile)
from mdkGradleWrapper.map(Wrapper.&getBatchScript)
into('gradle/wrapper/') {
from mdkGradleWrapper.map(Wrapper.&getJarFile)
from mdkGradleWrapper.map(Wrapper.&getPropertiesFile)
}
from(rootProject.file('mdk/')){
rootProject.file('mdk/gitignore.txt').eachLine {
if (!it.trim().isEmpty() && !it.trim().startsWith('#'))
exclude it
}
filter(ReplaceTokens, tokens: [
FORGE_VERSION: forgeVersion,
FORGE_GROUP: project.group,
FORGE_NAME: project.name,
MC_VERSION: minecraftVersion,
MAPPING_CHANNEL: mappingsChannel,
MAPPING_VERSION: mappingsVersion,
FORGE_SPEC_VERSION: gitversion.info.tag.split("\\.")[0],
MC_NEXT_VERSION: minecraftNextVersion,
EVENTBUS_VERSION: libs.versions.eventbus.get()
])
rename 'gitignore\\.txt', '.gitignore'
rename 'gitattributes\\.txt', '.gitattributes'
}
from(rootProject.file('src/test/java/com/example/examplemod/')) {
into('src/main/java/com/example/examplemod/')
}
from(rootProject.file('src/test/generated/mdk_datagen/')) {
into('src/main/resources/')
exclude '**/.cache/'
}
}
/*
tasks.named('javadoc', Javadoc) {
description = 'Generates the combined javadocs for the FML projects and the main Forge project'
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 })
var docsDir = rootProject.file('src/docs/')
inputs.dir(docsDir)
.withPropertyName('docs resources directory')
.withPathSensitivity(PathSensitivity.RELATIVE)
.optional()
failOnError = false
// Exclude the Minecraft classes if not enabled
if (!project.hasProperty('generateAllDocumentation')) {
exclude 'net/minecraft/**'
exclude 'com/mojang/**'
}
exclude 'mcp/**'
options {
stylesheetFile = new File(docsDir, 'stylesheet.css')
tags = [
'apiNote:a:<em>API Note:</em>',
'implSpec:a:<em>Implementation Requirements:</em>',
'implNote:a:<em>Implementation Note:</em>'
]
groups = [
'Forge Mod Loader': [
'net.minecraftforge.fml.common.asm*',
'net.minecraftforge.fml.loading*',
'net.minecraftforge.fml.server*'
],
'FML Core': [
'net.minecraftforge.fml',
'net.minecraftforge.fml.config*',
'net.minecraftforge.fml.event*',
'net.minecraftforge.fml.util*'
],
'FML Common': [
'net.minecraftforge.fml.core',
'net.minecraftforge.fml.event.config',
'net.minecraftforge.fml.event.lifecycle'
],
'FML Java/MC Language Providers': [
'net.minecraftforge.fml.common',
'net.minecraftforge.fml.javafmlmod',
'net.minecraftforge.fml.mclanguageprovider'
],
'Minecraft Forge API': [
'net.minecraftforge*'
]
]
author = false
noSince = true
noHelp = true
bottom = "Minecraft Forge is an open source modding API for Minecraft: Java Edition, licensed under the Lesser GNU General Public License, version 2.1."
windowTitle = "Minecraft Forge API ${version}"
docTitle = "Minecraft Forge API - ${forgeVersion} for Minecraft ${minecraftVersion}"
header = "<div style=\"margin-top: 9px;padding: 5px 6px;\"><strong>${forgeVersion} for Minecraft ${minecraftVersion}</strong></div>"
}
doLast {
project.copy {
from docsDir
exclude '/stylesheet.css'
into destinationDir
}
}
}
*/
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
artifact universalJar
artifact universalJarSrg
artifact forgedev.installer.jar
artifact mdkZip
artifact forgedev.userDev.jar
artifact sourcesJar
artifact serverShim.jar
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
/*
// These are debugging tasks that are only used when developing ForgeDevPlugin, to bulk test task's config/caching
// region ForgeDev Tests
def all = tasks.register('all') {}
afterEvaluate {
if (gradle.startParameter.taskNames.contains('all')) {
tasks.forEach { task -> // This forces everything to be realized, which is why we have the startParameter check
if (!gradle.startParameter.isTaskGraph() && task.name.startsWith('run')) return
if (task.name in [all.name, 'dependencyInsight', 'dependencies', 'dependencyUpdates', 'init', 'buildEnvironment', 'idea',
'eclipse', 'javaToolchains', 'outgoingVariants', 'projects', 'properties', 'resolvableConfigurations', 'tasks', 'updateDaemonJvm',
'generateActionsWorkflow'
]) return
if (task.name.startsWith('clean')) return
if (task.name == 'checkJarCompatibility') return // This doesn't work in FG6 dev, I think it's cuz we check vs srg names. Will need to look at later
all.configure { dependsOn(task) }
}
}
}
*/
// endregion
// We dont have any unit tests, so disable this error
tasks.withType(AbstractTestTask).configureEach {
failOnNoDiscoveredTests = false
}
// This shouldn't be needed according to the Gradle devs, as we never run these commands together.
// But in another case of Gradle being gradle, here we are
[ 'makePatches', 'compileJava', 'checkLicenseMain', 'updateLicenseMain', 'sourcesJar' ]
.each{ tasks.named(it).configure{ mustRunAfter forgedev.patches.apply }}
forgedev.runs.configureEach {
run.configure { dependsOn finalizeSpawn}
runTest.configure { dependsOn finalizeSpawn}
}
// run gradlew dependencyUpdates --no-parallel
//noinspection UnnecessaryQualifiedReference
tasks.named('dependencyUpdates', com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask) {
resolutionStrategy {
mcpBase.dependencies.get().forEach {dep -> force(dep) } // Force all vanilla dependencies to their correct versions
force(earlyDisplayLibs.slf4j.jdk14) // Force slf4j implementation, its tied to the api from the manifest
force('de.oceanlabs.mcp:mcp_config:' + bootLibs.versions.mcp) // Don't warn abou MCPConfig updates
}
}
+3
View File
@@ -0,0 +1,3 @@
/.gradle/
/build/
/out/
+13
View File
@@ -0,0 +1,13 @@
repositories {
maven { url = 'https://maven.minecraftforge.net/' }
mavenCentral()
}
dependencies {
implementation 'org.ow2.asm:asm:9.9.1'
implementation 'org.ow2.asm:asm-tree:9.9.1'
implementation 'net.minecraftforge:srgutils:0.6.2'
implementation 'commons-io:commons-io:2.13.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.7.0.202309050840-r'
}
@@ -0,0 +1,72 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.tasks.*
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.util.zip.ZipFile
abstract class BundleList extends DefaultTask {
@InputFiles abstract ConfigurableFileCollection getConfig()
@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)
}
}
@TaskAction
void run() {
def entries = [:] as TreeMap
def resolved = project.configurations.installer.resolvedConfiguration.resolvedArtifacts
for (def dep : resolved) {
def info = Util.getMavenInfoFromDep(dep)
//println("$dep.file.sha1\t$info.name\t$info.path")
entries.put("$info.art.group:$info.art.name", "$dep.file.sha256\t$info.name\t$info.path")
}
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
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
var classifier = 'server'
var genned = project.tasks.applyServerBinPatches
info = Util.getMavenInfoFromTask(genned, classifier)
file = genned.output.get().asFile
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
try (def zip = new ZipFile(serverBundle.get().asFile)) {
def entry = zip.getEntry('META-INF/libraries.list')
def data = zip.getInputStream(entry).text.split('\n')
for (def line : data) {
def (sha, artifact, path) = line.split('\t')
def (group, name, other) = artifact.split(':', 3)
//println("Group: $group Name: $name")
def key = "$group:$name"
if (!entries.containsKey(key))
entries.put(key, line)
}
}
output.get().asFile.text = entries.values().join('\n')
}
}
@@ -0,0 +1,52 @@
package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
@CompileStatic
abstract class BytecodeFinder extends DefaultTask {
@InputFile abstract RegularFileProperty getJar()
// It should be fine to mark the output as internal as we want to control when we run it anyways.
// This also shuts Gradle 8 up about implicit task dependencies.
@Internal abstract RegularFileProperty getOutput()
BytecodeFinder() {
output.convention(project.layout.buildDirectory.dir(name).map { it.file("output.json") })
}
@TaskAction
protected void exec() {
Util.init()
var outputFile = output.get().asFile
if (outputFile.exists())
outputFile.delete()
pre()
Util.processClassNodes(jar.get().asFile, this.&process)
post()
outputFile.text = new JsonBuilder(getData()).toPrettyString()
}
protected process(ClassNode node) {
if (node.fields !== null) node.fields.each { process(node, it) }
if (node.methods !== null) node.methods.each { process(node, it) }
}
protected pre() {}
protected process(ClassNode parent, FieldNode node) {}
protected process(ClassNode parent, MethodNode node) {}
protected post() {}
protected abstract Object getData()
}
@@ -0,0 +1,32 @@
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.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@CompileStatic
abstract class BytecodePredicateFinder extends BytecodeFinder {
@Internal
abstract Property<Closure<Boolean>> getPredicate()
private final Set<String> matches = new HashSet<>()
@Override
protected process(ClassNode parent, MethodNode node) {
for (final current : node.instructions) {
if (predicate.get().call(parent, node, current)) {
matches.add(parent.name)
return
}
}
}
@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.')}()
}
}
@@ -0,0 +1,97 @@
package net.minecraftforge.forge.tasks;
import groovy.transform.CompileDynamic;
import groovy.transform.CompileStatic;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeSet;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
/**
* Eclipse config files are literally just java properties, with the header cleaned up.
* https://github.com/eclipse/buildship/blob/5b2c7fca7fa86cd74d71b3c099c7b1559eba038e/org.eclipse.buildship.core/src/main/java/org/eclipse/buildship/core/internal/configuration/PreferenceStore.java#L238
*
* This does the same thing, as well as sorting alphabetically.
* It also ignores all comments. We can add them latter if someone cares.
*/
@CompileStatic
public class CleanProperties extends Properties {
private static final long serialVersionUID = 1L;
private static final String LINE_SEP = System.getProperty("line.separator");
private static final String UNIX_LINE_SEP = "\n";
private static final Charset ENCODING = StandardCharsets.UTF_8;
public CleanProperties load(File input) throws IOException {
if (input.exists()) {
try (Reader is = new InputStreamReader(new FileInputStream(input), ENCODING)) {
super.load(is);
}
}
return this;
}
public void store(File out) throws IOException {
if (!out.getParentFile().exists())
out.getParentFile().mkdirs();
try (OutputStream os = new FileOutputStream(out)) {
store(os, null);
}
}
@Override
public synchronized Enumeration<Object> keys() {
Set<Object> ret = new TreeSet<>();
for (Enumeration<?> e = super.keys(); e.hasMoreElements();)
ret.add(e.nextElement());
return Collections.enumeration(ret);
}
@CompileDynamic
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
Set<Map.Entry<Object, Object>> ret = new TreeSet<>((l, r) -> ((String)l.getKey()).compareTo((String)r.getKey()));
ret.addAll(super.entrySet());
return ret;
}
@Override
public void store(OutputStream out, String comments) throws IOException {
out.write(clean().getBytes(ENCODING));
out.flush();
}
@Override
public void store(Writer out, String comments) throws IOException {
out.write(clean());
out.flush();
}
private String clean() throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
try {
super.store(tmp, null);
} finally {
tmp.close();
}
String ret = tmp.toString(ENCODING).replace(LINE_SEP, UNIX_LINE_SEP);
ret = ret.substring(ret.indexOf(UNIX_LINE_SEP) + 1);
return ret;
}
}
@@ -0,0 +1,33 @@
package net.minecraftforge.forge.tasks
import groovy.transform.stc.FirstParam
import java.util.function.BiConsumer
public class ClosureHelper {
BiConsumer<String, Closure> callback
public ClosureHelper(Closure cl, BiConsumer<String, Closure> callback) {
this.callback = callback
apply(this, cl)
}
def methodMissing(String name, Object args) {
if (!args.class.isArray()) return
Object[] aargs = (Object[])args
if (aargs.length == 1 && aargs[0] instanceof Closure) {
this.callback.accept(name, (Closure)aargs[0])
} else {
throw new IllegalArgumentException('Unknown method: "' + name + '" with arguments ' + args + ' for ' + this)
}
}
static <T> T apply(T obj, @DelegatesTo(value = FirstParam, strategy = Closure.DELEGATE_FIRST) Closure cl) {
cl.delegate = obj
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
return obj
}
}
@@ -0,0 +1,54 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.OutputDirectory
import java.nio.file.Files
abstract class DownloadLibraries extends DefaultTask {
@InputFile abstract RegularFileProperty getInput()
@OutputDirectory abstract DirectoryProperty getOutput()
@OutputFile abstract RegularFileProperty getLibrariesOutput()
DownloadLibraries() {
output.convention(project.layout.buildDirectory.dir(name))
librariesOutput.convention(project.layout.buildDirectory.dir(name).map(d -> d.file('libraries.txt')))
}
@TaskAction
def run() {
Util.init()
File outputDir = output.get().asFile
var libraries = new ArrayList<String>()
def json = input.get().asFile.json().libraries.each { lib ->
//TODO: Thread?
def artifacts = [lib.downloads.artifact] + lib.downloads.get('classifiers', [:]).values()
artifacts.each{ art ->
def target = new File(outputDir, art.path)
libraries.add(target.absolutePath)
if (!target.exists() || art.sha1 != target.sha1()) {
project.logger.lifecycle("Downloading ${art.url}")
if (!target.parentFile.exists()) {
target.parentFile.mkdirs()
}
new URL(art.url).withInputStream { i ->
target.withOutputStream { it << i }
}
if (art.sha1 != target.sha1()) {
throw new IllegalStateException("Failed to download ${art.url} to ${target.canonicalPath} SHA Mismatch")
}
}
}
}
Files.write(librariesOutput.get().asFile.toPath(), libraries)
}
}
@@ -0,0 +1,28 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import java.util.zip.ZipFile
@CompileStatic
abstract class ExtractFile extends DefaultTask {
@InputFile abstract RegularFileProperty getInput()
@Input abstract Property<String> getTarget()
@OutputFile abstract RegularFileProperty getOutput()
@TaskAction
protected void exec() {
var zip = new ZipFile(input.get().asFile);
var entry = zip.getEntry(target.get())
if (entry == null)
throw new IllegalStateException(input.get().asFile.absolutePath + " does not contain " + target.get())
var stream = zip.getInputStream(entry)
output.get().asFile.withOutputStream { out ->
stream.transferTo(out)
}
}
}
@@ -0,0 +1,101 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import org.gradle.api.tasks.*
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
import static org.objectweb.asm.Opcodes.*
abstract class FieldCompareFinder extends BytecodeFinder {
@Nested
Map<String, Search> fields = [:] as HashMap
@Internal
Map<Search, String> fieldsReverse = [:] as HashMap
@Internal
Map<String, Set<ObjectTarget>> targets = [:] as TreeMap
@Override
protected pre() {
//fields.each{ k,v -> logger.lifecycle("Fields: " + k + ' ' + v) }
}
@Override
protected process(ClassNode parent, MethodNode node) {
AbstractInsnNode last = null
def parentInstance = new ObjectTarget(owner: parent.name, name: '', desc: '')
for (int x = 0; x < node.instructions.size(); x++) {
def current = node.instructions.get(x)
if (current.opcode === IF_ACMPEQ || current.opcode === IF_ACMPNE) {
if (last !== null && (last.opcode === GETSTATIC || last.opcode === GETFIELD)) {
def target = new Search(cls: last.owner, name: last.name)
def wanted = fieldsReverse.get(target)
def original = fields.get(wanted)
def instance = new ObjectTarget(owner: parent.name, name: node.name, desc: node.desc)
if (wanted !== null && (original.blacklist === null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
targets.computeIfAbsent(wanted, { k -> new TreeSet() }).add(instance)
}
}
}
last = current
}
}
@Internal
@Override
protected Object getData() {
def ret = [:] as HashMap
targets.forEach{ k, v ->
def e = fields.get(k)
ret[k] = [
cls: e.cls,
name: e.name,
replacement: e.replacement,
targets: v
]
}
return ret
}
@CompileStatic
@EqualsAndHashCode(excludes = ['replacement', 'blacklist'])
static class Search {
@Input
String cls
@Input
String name
@Input
String replacement
@Nested
@Optional
Set<ObjectTarget> blacklist
@Override
String toString() {
return cls + '.' + name
}
def blacklist(String owner, String name, String desc) {
if (blacklist === null)
blacklist = new HashSet<>()
blacklist.add(new ObjectTarget(owner: owner, name: name, desc: desc))
}
def blacklist(String owner) {
blacklist(owner, '', '')
}
}
void fields(Closure cl) {
new ClosureHelper(cl, {name, ccl ->
def search = ClosureHelper.apply(new Search(), ccl)
this.fields.put(name, search)
this.fieldsReverse.put(search, name)
})
}
}
@@ -0,0 +1,49 @@
package net.minecraftforge.forge.tasks
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
@CompileStatic
@TupleConstructor
final class InheritanceData implements Annotatable {
String name
int access
String superName
List<String> interfaces = []
Map<String, Method> methods = [:]
Map<String, Field> fields = [:]
List<Annotation> annotations = []
@TupleConstructor
static final class Method implements Annotatable {
int access
String override
List<Annotation> annotations = []
}
@TupleConstructor
static final class Field implements Annotatable {
int access
String desc
List<Annotation> annotations = []
}
@TupleConstructor
static final class Annotation {
String desc
}
private static final Gson GSON = new Gson()
static Map<String, InheritanceData> parse(File file) {
try (final reader = file.newReader()) {
return GSON.fromJson(reader, new TypeToken<Map<String, InheritanceData>>() {})
}
}
}
@CompileStatic
interface Annotatable {
List<InheritanceData.Annotation> getAnnotations()
}
@@ -0,0 +1,158 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.bundling.Zip
import org.gradle.api.tasks.*
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import groovy.json.JsonSlurper
abstract class InstallerJar extends Zip {
@Input @Optional abstract Property<Boolean> getFat()
@Input @Optional abstract Property<Boolean> getOffline()
InstallerJar() {
archiveClassifier.set('installer')
archiveExtension.set('jar') // Needs to be Zip task to not override Manifest, so set extension
destinationDirectory.set(project.layout.buildDirectory.dir('libs'))
fat.convention(false)
offline.convention(false)
def installerJson = project.tasks.installerJson
def launcherJson = project.tasks.launcherJson
def downloadInstaller = project.tasks.downloadInstaller
dependsOn(installerJson, launcherJson, downloadInstaller, project.configurations.installer)
from(installerJson, launcherJson)
from(project.rootProject.file('/src/main/resources/url.png'))
project.afterEvaluate {
from(project.zipTree(downloadInstaller.output)) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
if (fat.get() || offline.get()) {
def cfg = project.tasks.register(name + "Config", Configure)
cfg.get().configure {
parent = this
dependsOn(project.tasks.installerJson, project.tasks.launcherJson)
}
dependsOn(cfg)
} else {
// Things we ALWAYS bundle, this just the server shim jar, because the installer spec only says to extract the file.
// I should make it allow downloads but thats a spec break, and this is just a ~14KB jar
[
project.tasks.serverShimJar // Server bootstrap executable jar
].forEach { AbstractArchiveTask packed ->
def path = Util.getMavenInfoFromTask(packed).path
from(packed) {
rename { "maven/$path" }
}
}
}
}
}
static abstract class Configure extends DefaultTask {
public Zip parent
private int count = 0;
@TaskAction
protected void exec() {
def deps = [:] as java.util.TreeMap
// Gather all things that need downloading
[
project.tasks.installerJson,
project.tasks.launcherJson
].each { task ->
def json = task.output.get().asFile.json
json.libraries.each { lib ->
if (lib.downloads?.artifact?.url !== null && !lib.downloads.artifact.url.isEmpty())
deps.put(lib.name, lib.downloads.artifact)
}
}
// First find things we build in this project.
[
project.tasks.universalJar, // Forge runtime code
project.tasks.serverShimJar // Shim jar for dedicated server
].forEach { AbstractArchiveTask packed ->
def name = Util.getMavenInfoFromTask(packed).name
def info = deps.remove(name)
if (info !== null) {
println("Adding: $packed.path $name")
parent.from(packed) {
rename { "maven/$info.path" }
}
}
}
// Find any artifacts from the 'installer' config
// This config specifies the runtime files we intend for the interaller to have.
// And are typically what we would be developing and testing alongside Forge.
// So we may have local modified versions
def cfg = project.configurations.installer
while (cfg != null) {
//println('')
def resolved = cfg.resolvedConfiguration.resolvedArtifacts
int found = 0
for (def dep : resolved) {
def name = Util.getMavenInfoFromDep(dep).name
def info = deps.remove(name)
if (info == null) {
//println("Skipping: $name")
continue
}
//println("-$name")
found++
addFile(dep.file, info)
}
if (deps.isEmpty()) {
cfg = null
continue
}
// Prevent infinite loops if something fucky happens
if (found == 0)
throw new IllegalStateException("Failed to find any installer dependencies")
def seen = [] as Set
cfg = project.configurations.detachedConfiguration()
cfg.transitive = false
for (def key : deps.keySet()) {
def (group, artifact, other) = key.split(':', 3)
// Only resolve unique group:artifact so we don't get version overrides
if (seen.add(group + ':' + artifact)) {
//println("+$key")
cfg.dependencies.add(project.dependencies.create(key))
}
}
}
}
void addFile(file, info) {
boolean pack = parent.offline.get() || info.url.isEmpty()
// If it's a offline jar just always pack
if (!pack) {
try {
var remote = new URL("${info.url}.sha1").getText('UTF-8')
pack = info.sha1 != remote
} catch (FileNotFoundException e) {
pack = !info.url.startsWith('https://libraries.minecraft.net/')
// Oh noes its not there!
}
}
if (pack) {
println("Adding: $file.absolutePath")
parent.from(file) {
rename { "maven/$info.path" }
}
}
}
}
}
@@ -0,0 +1,73 @@
package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.nio.file.Files
abstract class InstallerJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input @Optional final Map<String, Object> libraries = new LinkedHashMap<>()
@Input Map<String, Object> json = new LinkedHashMap<>()
@InputFile abstract RegularFileProperty getIcon()
@Input abstract Property<String> getLauncherJsonName()
@Input abstract Property<String> getLogo()
@Input abstract Property<String> getMirrors()
@Input abstract Property<String> getWelcome()
InstallerJson() {
launcherJsonName.convention('/version.json')
logo.convention('/big_logo.png')
mirrors.convention('https://files.minecraftforge.net/mirrors-2.0.json')
welcome.convention("Welcome to the ${project.name.capitalize()} installer.")
output.convention(project.layout.buildDirectory.file('libs/install_profile.json'))
project.afterEvaluate {
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { packed ->
dependsOn(packed)
input.from packed.archiveFile
}
}
}
@TaskAction
protected void exec() {
def libs = libraries
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { AbstractArchiveTask packed ->
def info = Util.getMavenInfoFromTask(packed)
libs.put(info.name, [
name: info.name,
downloads: [
artifact: [
path: info.path,
url: "https://maven.minecraftforge.net/$info.path",
sha1: packed.archiveFile.get().asFile.sha1(),
size: packed.archiveFile.get().asFile.length()
]
]
])
}
json.libraries = libs.values().sort{a,b -> a.name.compareTo(b.name)}
json.icon = "data:image/png;base64," + new String(Base64.getEncoder().encode(Files.readAllBytes(icon.get().asFile.toPath())))
json.json = launcherJsonName.get()
json.logo = logo.get()
if (!mirrors.get().isEmpty())
json.mirrorList = mirrors.get()
json.welcome = welcome.get()
Files.writeString(output.get().getAsFile().toPath(), new JsonBuilder(json).toPrettyString())
}
}
@@ -0,0 +1,96 @@
package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.nio.file.Files
import static net.minecraftforge.forge.tasks.Util.getArtifacts
import static net.minecraftforge.forge.tasks.Util.iso8601Now
abstract class LauncherJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input Map<String, Object> json = new LinkedHashMap<>()
LauncherJson() {
output.convention(project.layout.buildDirectory.file('libs/version.json'))
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
def timestamp = iso8601Now()
json.putAll([
_comment: [
"Please do not automate the download and installation of Forge.",
"Our efforts are supported by ads from the download page.",
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
],
id: "$mc-$project.name-$forge",
time: timestamp,
releaseTime: timestamp,
inheritsFrom: mc,
type: 'release',
logging: [:],
mainClass: '',
libraries: []
] as LinkedHashMap)
[
project.tasks.universalJar
].forEach { packed ->
dependsOn(packed)
input.from packed.archiveFile
}
def patched = project.tasks.applyClientBinPatches
dependsOn(patched)
input.from patched.output
}
}
@TaskAction
protected void exec() {
var packed = (AbstractArchiveTask) project.tasks.universalJar
def info = Util.getMavenInfoFromTask(packed)
json.libraries.add([
name: info.name,
downloads: [
artifact: [
path: info.path,
url: "https://maven.minecraftforge.net/$info.path",
sha1: packed.archiveFile.get().asFile.sha1(),
size: packed.archiveFile.get().asFile.length()
]
]
])
var classifier = 'client'
var genned = project.tasks.applyClientBinPatches
info = Util.getMavenInfoFromTask(genned, classifier)
json.libraries.add([
name: info.name,
downloads: [
artifact: [
path: info.path,
url: "",
sha1: genned.output.get().asFile.sha1(),
size: genned.output.get().asFile.length()
]
]
])
json.libraries.addAll(getArtifacts(project.configurations.installerextra).values())
json.libraries.addAll(getArtifacts(project.configurations.installer).values())
Files.writeString(output.get().asFile.toPath(), new JsonBuilder(json).toPrettyString())
}
}
@@ -0,0 +1,46 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import org.apache.commons.io.IOUtils
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@CompileStatic
abstract class MergeJars extends DefaultTask {
MergeJars() {
output.convention(project.layout.buildDirectory.dir(name).map { it.file('output.jar') })
}
@TaskAction
void run() {
def jars = inputJars.files
try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output.get().asFile))) {
for (def jar : jars) {
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(jar))) {
ZipEntry entry
while ((entry = zin.getNextEntry()) !== null) {
ZipEntry _new = new ZipEntry(entry.getName())
_new.setTime(0) //SHOULD be the same time as the main entry, but NOOOO _new.setTime(entry.getTime()) throws DateTimeException, so you get 0, screw you!
zout.putNextEntry(_new)
IOUtils.copy(zin, zout)
}
}
}
}
}
@InputFiles
abstract ConfigurableFileCollection getInputJars()
@OutputFile
abstract RegularFileProperty getOutput()
}
@@ -0,0 +1,32 @@
package net.minecraftforge.forge.tasks
import groovy.transform.EqualsAndHashCode
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
@EqualsAndHashCode
public class ObjectTarget implements Comparable<ObjectTarget> {
@Input
String owner
@Input
String name
@Input
@Optional
String desc
@Override
String toString() {
if (desc == null)
return owner + '.' + name
return owner + '.' + name + desc
}
@Override
int compareTo(ObjectTarget o) {
return toString() <=> o.toString()
}
}
@@ -0,0 +1,47 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.artifacts.Dependency
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.nio.file.Files
import java.nio.file.StandardCopyOption
abstract class SetupCheckJarCompatibility extends DefaultTask {
SetupCheckJarCompatibility() {
group = 'jar compatibility'
onlyIf {
inputVersion.getOrNull() !== null
}
outputs.upToDateWhen { false } // Never up to date, because this setup task should always run
baseBinPatchesOutput.convention(project.layout.buildDirectory.dir(name).map { it.file('joined.lzma') })
}
@TaskAction
void run() {
def inputVersion = inputVersion.get()
def baseForgeUserdev = project.layout.buildDirectory.dir(name).map { it.file("forge-${inputVersion}-userdev.jar") }.get().asFile
project.rootProject.extensions.download.run {
src "https://maven.minecraftforge.net/net/minecraftforge/forge/${inputVersion}/forge-${inputVersion}-userdev.jar"
dest baseForgeUserdev
}
def joinedLzma = project.zipTree(baseForgeUserdev).matching { it.include('joined.lzma') }.singleFile
Files.copy(joinedLzma.toPath(), baseBinPatchesOutput.get().asFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
@Input
@Optional
abstract Property<String> getInputVersion()
@OutputFile
abstract RegularFileProperty getBaseBinPatchesOutput()
}
@@ -0,0 +1,74 @@
package net.minecraftforge.forge.tasks
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import groovy.transform.CompileStatic
import org.eclipse.jgit.api.Git
import javax.annotation.Nullable
import java.util.stream.Collectors
@CompileStatic
class TeamcityRequests {
public static final Gson GSON = new Gson()
@Nullable
static <T> T jsonRequest(TypeToken<T> clazz, String url) throws Exception {
final HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection()
conn.setRequestProperty('Accept', 'application/json')
conn.setReadTimeout(5 * 1000)
conn.setConnectTimeout(5 * 1000)
conn.connect()
if (conn.responseCode !== 200) {
return null
}
try (final InputStream is = conn.getInputStream()) {
GSON.fromJson(new InputStreamReader(is), clazz.getType())
}
}
@Nullable
static Map<String, Build> buildsByCommit() throws IOException {
final Map<String, Build> builds = [:]
jsonRequest(new TypeToken<Builds>() {}, 'https://teamcity.minecraftforge.net/guestAuth/app/rest/builds?fields=build:(revisions,number)&locator=buildType:(id:MinecraftForge_MinecraftForge_MinecraftForge_MinecraftForge__Build),defaultFilter:false,count:300,status:SUCCESS')
?.build?.forEach {
it.revisions.revision.each { rev -> builds[rev.version] = it }
}
return builds
}
@Nullable
static String attemptFindBase(File gitPath) {
try (final git = Git.open(gitPath)) {
final Map<String, Build> buildByCommit = buildsByCommit()
for (final commit in git.log().setMaxCount(100).call()) {
// Find the first commit which was built on CI
final build = buildByCommit[commit.id.name]
if (build) {
return build.number
}
}
} catch (Exception ignored) {}
return null
}
static final class Builds {
public List<Build> build
}
static final class Build {
public String number
public Revisions revisions
}
static final class Revisions {
public int count
public List<Revision> revision
}
static final class Revision {
public String version
}
}
@@ -0,0 +1,230 @@
package net.minecraftforge.forge.tasks
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Semaphore
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
final class Util {
public static final int ASM_LEVEL = Opcodes.ASM9
private static final HttpClient HTTP = HttpClient.newBuilder().build()
static void init() {
File.metaClass.sha1 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-1')
delegate.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
File.metaClass.getSha1 = { !delegate.exists() ? null : delegate.sha1() }
File.metaClass.sha256 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-256')
delegate.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
File.metaClass.getSha256 = { !delegate.exists() ? null : delegate.sha256() }
File.metaClass.json = { -> new JsonSlurper().parseText(delegate.text) }
File.metaClass.getJson = { return delegate.exists() ? new JsonSlurper().parse(delegate) : [:] }
File.metaClass.setJson = { json -> delegate.text = new JsonBuilder(json).toPrettyString() }
Date.metaClass.iso8601 = { ->
var format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
var result = format.format(delegate)
return result[0..21] + ':' + result[22..-1]
}
String.metaClass.rsplit = { String del, int limit = -1 ->
var lst = new ArrayList<String>()
int x = 0
int idx
String tmp = delegate
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit === -1 || x++ < limit)) {
lst.add(0, tmp.substring(idx + del.length(), tmp.length()))
tmp = tmp.substring(0, idx)
}
lst.add(0, tmp)
return lst
}
}
static String[] getClasspath(Project project, Map libs, String artifact) {
def ret = []
artifactTree(project, artifact).each { key, lib ->
libs[lib.name] = lib
if (lib.name != artifact)
ret.add(lib.name)
}
return ret
}
@CompileStatic
static Map getArtifacts(Configuration config) {
var ret = [:]
var semaphore = new Semaphore(1, true)
config.resolvedConfiguration.resolvedArtifacts.parallelStream().forEachOrdered(dep -> {
var info = getMavenInfoFromDep(dep)
var domain = 'libraries.minecraft.net'
var url = "https://$domain/$info.path"
if (!checkExists(url))
url.values[0] = 'maven.minecraftforge.net'
var sha1 = sha1(dep.file)
semaphore.acquire()
ret[info.key] = [
name: info.name,
downloads: [
artifact: [
path: info.path,
url: url.toString(),
sha1: sha1,
size: dep.file.length()
]
]
]
semaphore.release()
})
return ret
}
@CompileStatic
static Map getMavenInfoFromDep(ResolvedArtifact dep) {
return getMavenInfoFromMap([
group: dep.moduleVersion.id.group,
name: dep.moduleVersion.id.name,
version: dep.moduleVersion.id.version,
classifier: dep.classifier,
extension: dep.extension
])
}
@CompileStatic
static Map getMavenInfoFromTask(AbstractArchiveTask task) {
return getMavenInfoFromMap([
group: task.project.group.toString(),
name: task.project.name,
version: task.project.version.toString(),
classifier: task.archiveClassifier.get(),
extension: task.archiveExtension.get()
])
}
@CompileStatic
static Map getMavenInfoFromTask(Task task, String classifier) {
return getMavenInfoFromMap([
group: task.project.group.toString(),
name: task.project.name,
version: task.project.version.toString(),
classifier: classifier,
extension: 'jar'
])
}
@CompileStatic
private static Map getMavenInfoFromMap(Map<String, String> art) {
var key = "$art.group:$art.name"
var name = "$art.group:$art.name:$art.version"
var path = "${art.group.replace('.', '/')}/$art.name/$art.version/$art.name-$art.version"
if (art.classifier !== null) {
name += ":$art.classifier"
path += "-$art.classifier"
}
if ('jar' != art.extension) {
name += "@$art.extension"
path += ".$art.extension"
} else {
path += ".jar"
}
return [
key: key.toString(),
name: name.toString(),
path: path.toString(),
art: art
]
}
static String iso8601Now() { new Date().iso8601() }
@CompileStatic
static String sha1(File file) {
MessageDigest md = MessageDigest.getInstance('SHA-1')
file.eachByte(4096) { byte[] bytes, int size ->
md.update(bytes, 0, size)
}
return md.digest().collect(this.&toHex).join('')
}
@CompileStatic
private static String toHex(byte bite) {
return String.format('%02x', bite)
}
private static Map artifactTree(Project project, String artifact, boolean transitive = true) {
if (!project.ext.has('tree_resolver'))
project.ext.tree_resolver = 1
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
cfg.transitive = transitive
def dep = project.dependencies.create(artifact)
cfg.dependencies.add(dep)
def files = cfg.resolve()
return getArtifacts(cfg)
}
@CompileStatic
static boolean checkExists(String url) {
try {
return HTTP.send(HttpRequest.newBuilder(new URI(url))
.method('HEAD', HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.discarding()
).statusCode() === 200
} catch (Exception e) {
if (e.toString().contains('unable to find valid certification path to requested target'))
throw new RuntimeException("Failed to connect to $url: Missing certificate root authority, try updating Java")
throw e
}
}
static String getLatestForgeVersion(String mcVersion) {
final json = new JsonSlurper().parseText(new URL('https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json').getText('UTF-8'))
final ver = json.promos["$mcVersion-latest"]
ver === null ? null : (mcVersion + '-' + ver)
}
@CompileStatic
static void processClassNodes(File file, @ClosureParams(value = SimpleType, options = 'org.objectweb.asm.tree.ClassNode') Closure process) {
file.withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
ZipEntry zein
while ((zein = zin.nextEntry) !== null) {
if (zein.name.endsWith('.class')) {
var node = new ClassNode(ASM_LEVEL)
new ClassReader(zin).accept(node, 0)
process(node)
}
}
}
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import net.minecraftforge.srgutils.MinecraftVersion
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
abstract class ValidateDeprecations extends DefaultTask {
@InputFile
abstract RegularFileProperty getInput()
@Input
abstract Property<String> getMcVersion()
ValidateDeprecations() {
this.onlyIf { !System.env.TEAMCITY_VERSION }
}
@TaskAction
protected void exec() {
var mcVer = MinecraftVersion.from(mcVersion.get())
List<String> errors = []
Util.processClassNodes(input.get().asFile) {
processNode(mcVer, errors, it)
}
if (!errors.isEmpty()) {
errors.forEach {
this.logger.error("Deprecated ${it[0]} is marked for removal in ${it[1]} but is not yet removed")
}
throw new IllegalStateException("Found deprecated members marked for removal but not yet removed in ${mcVer}; see log for details")
}
}
protected processNode(MinecraftVersion mcVer, List<String> errors, ClassNode node) {
node.visibleAnnotations?.each { annotation ->
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
"class ${node.name}"
}
}
node.fields?.each { field ->
field.visibleAnnotations?.each { annotation ->
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
"field ${node.name}#${field.name}"
}
}
}
node.methods?.each { method ->
method.visibleAnnotations?.each { annotation ->
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
"method ${node.name}#${method.name}${method.desc}"
}
}
}
}
private static void processAnnotations(AnnotationNode annotation, MinecraftVersion mcVer, List<String> errors, Closure<String> context) {
def values = annotation.values
if (values === null)
return
int forRemoval = values.indexOf('forRemoval')
int since = values.indexOf('since')
if (annotation.desc == 'Ljava/lang/Deprecated;' && forRemoval !== -1 && since !== -1 && values.size() >= 4 && values[forRemoval + 1] === true) {
def oldVersion = MinecraftVersion.from(values[since + 1])
int[] split = ValidateDeprecations.splitDots(oldVersion.toString())
if (split.length < 2)
return
def removeVersion = MinecraftVersion.from("${split[0]}.${split[1] + 1}")
if (removeVersion <= mcVer)
errors.add([context(), removeVersion])
}
}
@CompileStatic
private static int[] splitDots(String version) {
String[] pts = version.split('\\.')
int[] values = new int[pts.length]
for (int x = 0; x < pts.length; x++)
values[x] = Integer.parseInt(pts[x])
return values
}
}
@@ -0,0 +1,310 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
import net.minecraftforge.forge.tasks.InheritanceData
import net.minecraftforge.srgutils.IMappingFile
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.objectweb.asm.Opcodes
@CompileStatic
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')
}
}
}
private static TreeMap<String, ATParser.Entry> process(File file, Reporter reporter, Map<String, InheritanceData> inheritance) {
final TreeMap<String, ATParser.Entry> lines = ATParser.parse(file.readLines(), reporter)
final Map<String, ATParser.Entry> constructorGroups = [:]
final itr = lines.entrySet().iterator()
final toRemove = []
while (itr.hasNext()) {
final next = itr.next()
String key = next.key
final entry = next.value
if (entry === null) continue
final binaryName = entry.cls.replaceAll('\\.', '/')
// Process Groups, this will remove any entries outside the group that is covered by the group
if (entry.group) {
final jcls = inheritance[binaryName]
if (jcls === null) {
itr.remove()
reporter.report("Invalid group: $key")
} else if ('*' == entry.desc) {
if (!jcls.fields) {
itr.remove()
reporter.report("Invalid group, class has no fields: $key")
} else {
jcls.fields.each { field, value ->
final fkey = entry.cls + ' ' + field
if (accessLevel(value.access) < accessStr(entry.modifier)) {
if (lines.containsKey(fkey)) {
toRemove.add(fkey)
} else if (!entry.existing.contains(fkey)) {
reporter.report("Missing group entry: $fkey")
}
entry.children.add(fkey)
} else if (lines.containsKey(fkey)) {
toRemove.add(fkey)
reporter.report("Found invalid group entry: $fkey")
}
}
entry.existing.findAll { it !in entry.children }.each { println('Removed: ' + it) }
}
} else if ('*()' == entry.desc) {
if (!jcls.methods) {
itr.remove()
reporter.report("Invalid group, class has no methods: $key")
} else {
jcls.methods.each { mtd, value ->
if (mtd.startsWith('<clinit>') || mtd.startsWith('lambda$')) return
key = entry.cls + ' ' + mtd.replace(' ', '')
if (accessLevel(value.access) < accessStr(entry.modifier)) {
if (lines.containsKey(key)) {
toRemove.add(key)
} else if (!entry.existing.contains(key)) {
reporter.report("Missing group entry: $key")
}
entry.children.add(key)
} else if (lines.containsKey(key)) {
toRemove.add(key)
reporter.report("Found invalid group entry: $key")
}
}
entry.existing.findAll { it !in entry.children }.each { println('Removed: ' + it) }
}
} else if ('<init>' == entry.desc) { //Make all public non-abstract subclasses
constructorGroups.put(binaryName, entry)
}
}
// Process normal lines, remove invalid and remove narrowing
else {
def jcls = inheritance.get(binaryName)
if (jcls === null) {
itr.remove()
reporter.report("Invalid: $key")
} else if (entry.desc == '') {
if (accessLevel(jcls.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
itr.remove()
reporter.report("Invalid Narrowing: $key")
}
} else if (!entry.desc.contains('(')) {
if (!jcls.fields || !jcls.fields.containsKey(entry.desc)) {
itr.remove()
reporter.report("Invalid: $key")
} else {
final value = jcls.fields[entry.desc]
if (accessLevel(value.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
itr.remove()
reporter.report("Invalid Narrowing: $key - ${entry.comment}")
}
}
} else {
final jdesc = entry.desc.replace('(', ' (')
if (!jcls.methods || !jcls.methods.containsKey(jdesc)) {
itr.remove()
reporter.report("Invalid: $key")
} else {
final value = jcls.methods[jdesc]
if (accessLevel(value.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
itr.remove()
reporter.report("Invalid Narrowing: $key")
}
}
}
}
}
inheritance.each { tcls, value ->
if (!value.methods || ((value.access & Opcodes.ACC_ABSTRACT) !== 0)) return
String parent = tcls
while (parent !== null) {
constructorGroups[parent]?.tap { entry ->
value.methods.each { mtd, v ->
if (mtd.startsWith('<init>')) {
final child = tcls.replaceAll('/', '\\.') + ' ' + mtd.replace(' ', '')
if (accessLevel(v.access) < 3) {
if (lines.containsKey(child)) {
toRemove.add(child)
} else if (child !in entry.existing) {
reporter.report("Missing group entry: $child")
}
entry.children.add(child)
} else if (lines.containsKey(child)) {
toRemove.add(child)
reporter.report("Found invalid group entry: $child")
}
}
}
}
parent = inheritance[parent]?.superName
}
}
constructorGroups.values().each { entry -> entry.existing.findAll { it !in entry.children }.each{ reporter.report("Found invalid group entry: $it") } }
toRemove.each(lines.&remove)
return lines
}
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance, IMappingFile mappings) {
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)
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)
data.add(line + (comment ? ' ' + comment : ''))
}
data.add('#endgroup')
}
}
return data
}
static int accessStr(String access) {
if (access.endsWith('-f') || access.endsWith('+f')) return 4
switch (access.toLowerCase()) {
case 'public': return 3
case 'protected': return 2
case 'default': return 1
case 'private': return 0
default: return -1
}
}
static int accessLevel(int access) {
if ((access & Opcodes.ACC_PUBLIC) !== 0) return 3
if ((access & Opcodes.ACC_PROTECTED) !== 0) return 2
if ((access & Opcodes.ACC_PRIVATE) !== 0) return 0
return 1
}
}
@CompileStatic
class ATParser {
static TreeMap<String, Entry> parse(List<String> lines, CheckTask.Reporter reporter) {
TreeMap<String, Entry> outLines = new TreeMap<>()
Entry group = null
for (final line : lines) {
if (line.isEmpty()) continue
if (line.startsWith('#group ')) {
final entry = parseEntry(line.substring(7))
if (entry.desc != '*' && entry.desc != '*()' && entry.desc != '<init>') {
reporter.report("Invalid group: $line", false)
}
entry.group = true
entry.children = []
entry.existing = []
group = entry
if (outLines.containsKey(entry.key)) {
reporter.report("Duplicate group: $line", false)
}
outLines[entry.key] = group
} else if (group !== null) {
if (line.startsWith('#endgroup')) {
group = null
} else {
final key = parseEntry(line).key
group.existing.add(key)
}
} else if (line.startsWith('#endgroup')) {
reporter.report("Invalid group ending: $line", false)
} else if (line.startsWith('#')) {
//Nom
} else {
final entry = parseEntry(line)
if (outLines.containsKey(entry.key)) {
reporter.report("Found duplicate: $line")
continue
}
outLines[entry.key] = entry
}
}
return outLines
}
static Entry parseEntry(String line) {
final idx = line.indexOf('#')
final String comment = idx === -1 ? null : line.substring(idx)
if (idx !== -1) line = line.substring(0, idx - 1)
final data = (line.trim() + ' ').split(' ', -1)
new Entry(data[0], data[1], data[2], comment)
}
@TupleConstructor
static final class Entry {
String modifier, cls, desc, comment
Set<String> existing
TreeSet<String> children
boolean group = false
@Lazy
String key = {cls + (desc.isEmpty() ? '' : ' ' + desc)}()
Object getAt(String key) {
return getProperty(key)
}
}
}
@@ -0,0 +1,95 @@
package net.minecraftforge.forge.tasks.checks
import net.minecraftforge.forge.tasks.Util
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
abstract class CheckExcs extends CheckTask {
@InputFile abstract RegularFileProperty getBinary()
@InputFiles abstract ConfigurableFileCollection getExcs()
@Override
void check(Reporter reporter, boolean fix) {
final Set<String> known = []
collectKnown(known)
excs.each { f ->
final lines = []
f.eachLine { line ->
int idx = line.indexOf('#')
if (idx === 0 || line.isEmpty()) {
return
}
if (idx !== -1) line = line.substring(0, idx - 1)
if (!line.contains('=')) {
reporter.report("Invalid: $line")
return
}
def (String key, String value) = line.split('=', 2)
if (!known.contains(key)) {
reporter.report("Unknown: $line")
return
}
String desc = key.split('\\.', 2)[1]
if (!desc.contains('(')) {
reporter.report("Invalid: $line")
return
}
desc = '(' + desc.split('\\(', 2)[1]
def (exceptions, String args) = value.contains('|') ? value.split('|', 2) : [value, '']
if (args.split(',').length !== Type.getArgumentTypes(desc).length) {
reporter.report("Invalid: $line")
return
}
lines.add(line)
return
}
if (fix) f.text = lines.sort().join('\n')
}
}
private void collectKnown(Collection<String> known) {
binary.get().asFile.withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
final visitor = new ClassVisitor(Util.ASM_LEVEL) {
private String cls
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.cls = name
}
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
known.add(this.cls + '.' + name + descriptor)
super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
ZipEntry zein
while ((zein = zin.nextEntry) !== null) {
if (zein.name.endsWith('.class')) {
ClassReader reader = new ClassReader(zin)
reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)
}
}
}
}
}
}
@@ -0,0 +1,11 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
@CompileStatic
enum CheckMode {
CHECK,
FIX
CheckMode() {}
}
@@ -0,0 +1,182 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Optional
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Pattern
@CompileStatic
abstract class CheckPatches extends CheckTask {
private static final Pattern HUNK_START_PATTERN = Pattern.compile('^@@ -[0-9,]* \\+[0-9,_]* @@$')
private static final Pattern WHITESPACE_PATTERN = Pattern.compile('^[+\\-]\\s*$')
private static final Pattern IMPORT_PATTERN = Pattern.compile('^[+\\-]\\s*import.*')
private static final Pattern FIELD_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^=;]*)(=.*)?;\\s*$')
private static final Pattern METHOD_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^(]*)[(]([^)]*)?[)]\\s*[{]\\s*$')
private static final Pattern CLASS_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final[\\s]*)?(class|interface)([^{]*)[{]\\s*$')
private static final Map<String, Integer> ACCESS_MAP = [private: 0, protected: 2, public: 3].tap { it.put(null, 1) }
@InputDirectory abstract DirectoryProperty getPatchDir()
@Input @Optional abstract ListProperty<String> getPatchesWithS2SArtifact()
@Override
void check(Reporter reporter, boolean fix) {
final patchDir = getPatchDir().get().asFile.toPath()
Files.walk(patchDir).withCloseable {
it.filter(Files.&isRegularFile).forEach { path ->
final String relativeName = patchDir.relativize(path).toString()
verifyPatch(path, reporter, fix, relativeName, patchesWithS2SArtifact.get().contains(relativeName.replace('\\', '/')))
}
}
}
static boolean accessChange(String previous, String current) {
//return ACCESS_MAP[previous] < ACCESS_MAP[current]
return previous != current
}
void verifyPatch(Path patch, Reporter reporter, boolean fix, String patchPath, boolean hasS2SArtifact) {
int oldFixedErrors = reporter.fixed.size()
final lines = Files.readAllLines(patch)
int hunksStart = 0
boolean onlyWhiteSpace = false
final List<String> newLines = []
// First two lines are file name ++/-- and we do not care
newLines.add(lines[0] + '\n')
newLines.add(lines[1] + '\n')
int i
for (i = 2; i < lines.size(); ++i) {
def line = lines[i]
newLines.add(line + '\n')
if (HUNK_START_PATTERN.matcher(line).find()) {
if (onlyWhiteSpace) {
if (!hasS2SArtifact)
reporter.report("Patch contains only white space hunk starting at line ${hunksStart + 1}, file: $patchPath")
int toRemove = i - hunksStart
while (toRemove-- > 0)
newLines.remove(newLines.size() - 1)
}
hunksStart = i
onlyWhiteSpace = true
continue
}
if (line.startsWithAny('+','-')) {
def prefixChange = false
def prevLine = lines[i - 1]
if (line.charAt(0) == (char)'+' && prevLine.charAt(0) == (char)'-') {
def prevTrim = prevLine.substring(1).replaceAll("\\s", "")
def currTrim = line.substring(1).replaceAll("\\s", "")
if (prevTrim == currTrim) {
prefixChange = true
}
def pMatcher = FIELD_PATTERN.matcher(prevLine)
def cMatcher = FIELD_PATTERN.matcher(line)
if (pMatcher.find() && cMatcher.find() &&
pMatcher.group(6) == cMatcher.group(6) && // = ...
pMatcher.group(5) == cMatcher.group(5) && // field name
pMatcher.group(3) == cMatcher.group(3) && // static
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
}
pMatcher = METHOD_PATTERN.matcher(prevLine)
cMatcher = METHOD_PATTERN.matcher(line)
if (pMatcher.find() && cMatcher.find() &&
pMatcher.group(6) == cMatcher.group(6) && // params
pMatcher.group(5) == cMatcher.group(5) && // <T> void name
pMatcher.group(3) == cMatcher.group(3) && // static
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
}
pMatcher = CLASS_PATTERN.matcher(prevLine)
cMatcher = CLASS_PATTERN.matcher(line)
if (pMatcher.find() && cMatcher.find() &&
pMatcher.group(6) == cMatcher.group(6) && // ClassName<> extends ...
pMatcher.group(5) == cMatcher.group(5) && // class | interface
pMatcher.group(3) == cMatcher.group(3) && // static
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
}
}
if (line.charAt(0) == (char)'-' && i + 1 < lines.size()) {
final nextLine = lines[i + 1]
if (nextLine.charAt(0) == (char)'+') {
final nextTrim = nextLine.substring(1).replaceAll("\\s", "")
final currTrim = line.substring(1).replaceAll("\\s", "")
if (nextTrim == currTrim) {
prefixChange = true
}
}
}
final isWhiteSpaceChange = WHITESPACE_PATTERN.matcher(line).find()
if (!prefixChange && !isWhiteSpaceChange) {
onlyWhiteSpace = hasS2SArtifact && IMPORT_PATTERN.matcher(line).find()
} else if (isWhiteSpaceChange) {
final prevLineChange = prevLine.startsWithAny('+','-')
final nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-')
if (!prevLineChange && !nextLineChange) {
reporter.report("Patch contains white space change in valid hunk at line ${i + 1}, file: $patchPath \n$prevLine\n$line\n${lines[i+1]}", false)
}
}
if (line.contains('\t')) {
reporter.report("Patch contains tabs on line ${i + 1}, file: $patchPath")
line = line.replaceAll('\t', ' ')
newLines.remove(newLines.size() - 1)
newLines.add(line + '\n')
}
if (IMPORT_PATTERN.matcher(line).find() && !hasS2SArtifact) {
reporter.report("Patch contains import change on line ${i + 1}, file: $patchPath", false)
}
}
}
if (onlyWhiteSpace) {
if (!hasS2SArtifact)
reporter.report("Patch contains only white space hunk starting at line ${hunksStart + 1}, file: $patchPath")
def toRemove = i - hunksStart;
while (toRemove-- > 0)
newLines.remove(newLines.size() - 1)
}
if ((reporter.fixed.size() > oldFixedErrors && fix) || hasS2SArtifact) {
if (newLines.size() <= 2) {
logger.lifecycle("Patch is now empty removing, file: {}", patchPath)
Files.delete(patch)
}
else {
if (!hasS2SArtifact)
logger.lifecycle("*** Updating patch file. Please run setup then genPatches again. ***")
Files.newBufferedWriter(patch).withCloseable {
newLines.each { l -> it.write(l) }
}
}
}
}
}
@@ -0,0 +1,110 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
import net.minecraftforge.forge.tasks.Annotatable
import net.minecraftforge.forge.tasks.InheritanceData
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
@CompileStatic
abstract class CheckSAS extends CheckTask {
@InputFile abstract RegularFileProperty getInheritance()
@InputFiles abstract ConfigurableFileCollection getSass()
@Override
void check(Reporter reporter, boolean fix) {
final inheritance = InheritanceData.parse(this.inheritance.get().asFile)
sass.each { f ->
final lines = []
f.eachLine { line ->
if (line[0] == '\t') return // Skip any tabbed lines, those are ones we add
final idx = line.indexOf('#')
if (idx == 0 || line.isEmpty()) {
lines.add(line)
return
}
def comment = idx == -1 ? null : line.substring(idx)
if (idx != -1) line = line.substring(0, idx - 1)
final spl = (line.trim().replace('(', ' (') + ' ').split(' ', -1)
def (String cls, String name, String desc) = [spl[0], spl[1], spl[2]]
cls = cls.replaceAll('\\.', '/')
if (inheritance[cls] === null) {
reporter.report("Invalid: $line")
} else if (name.isEmpty()) { //Class SAS
final toAdd = []
final clsSided = isSided(inheritance[cls])
boolean sided = clsSided
/* TODO: MergeTool doesn't do fields
if (json[cls]['fields'] != null) {
for (entry in json[cls]['fields']) {
if (isSided(entry.value)) {
sided = true
toAdd.add('\t' + cls + ' ' + entry.key)
}
}
} */
final clsInh = inheritance[cls]
if (clsInh.methods) {
for (entry in clsInh.methods) {
if (isSided(entry.value)) {
sided = true
toAdd.add('\t' + cls + ' ' + entry.key.replaceAll(' ', ''))
findChildMethods(inheritance, cls, entry.key).each { lines.add('\t' + it) }
findChildMethods(inheritance, cls, entry.key).each { println(line + ' -- ' + it) }
} else if (clsSided) {
findChildMethods(inheritance, cls, entry.key).each { lines.add('\t' + it) }
findChildMethods(inheritance, cls, entry.key).each { println(line + ' -- ' + it) }
}
}
}
if (sided) {
lines.add(cls + (comment == null ? '' : ' ' + comment))
lines.addAll(toAdd.sort())
} else {
reporter.report("Invalid: $line")
}
} else if (desc.isEmpty()) { // Fields
/* TODO: MergeTool doesn't do fields
if (json[cls]['fields'] != null && isSided(json[cls]['fields'][name]))
lines.add(cls + ' ' + name + (comment == null ? '' : ' ' + comment))
else */
reporter.report("Invalid: $line")
} else { // Methods
final clsInh = inheritance[cls]
if (clsInh.methods === null || !isSided(clsInh.methods[name + ' ' + desc]))
reporter.report("Invalid: $line")
else {
lines.add(cls + ' ' + name + desc + (comment == null ? '' : ' ' + comment))
findChildMethods(inheritance, cls, name + ' ' + desc).each { println(line + ' -- ' + it) }
findChildMethods(inheritance, cls, name + ' ' + desc).each { lines.add('\t' + it) }
}
}
}
if (fix) f.text = lines.join('\n')
}
}
protected static boolean isSided(Annotatable annotatable) {
if (annotatable === null) return false
for (ann in annotatable.annotations) {
if ('Lnet/minecraftforge/api/distmarker/OnlyIn;' == ann.desc)
return true
}
return false
}
protected static findChildMethods(Map<String, InheritanceData> json, String cls, String desc) {
return json.values().findAll{ it.methods !== null && it.methods[desc] !== null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
.collect { it.name + ' ' + desc.replace(' ', '') } as TreeSet
}
}
@@ -0,0 +1,114 @@
package net.minecraftforge.forge.tasks.checks
import groovy.transform.CompileStatic
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.ThirdParam
import net.minecraftforge.forge.tasks.Util
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.logging.LogLevel
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.VerificationTask
@CompileStatic
abstract class CheckTask extends DefaultTask implements VerificationTask {
@Input
abstract Property<CheckMode> getMode()
private boolean ignoreFailures = false
@Input
@Override
boolean getIgnoreFailures() {
return ignoreFailures
}
@Override
void setIgnoreFailures(boolean ignoreFailures) {
this.ignoreFailures = ignoreFailures
}
@TaskAction
void run() {
Util.init()
boolean doFix = getMode().get() === CheckMode.FIX
final Reporter reporter = new Reporter(doFix)
check(reporter, doFix)
if (reporter.messages) {
if (getMode().get() === CheckMode.CHECK) {
logger.error("Check task '{}' found errors:\n{}", name, reporter.messages.join('\n'))
if (!ignoreFailures) {
throw new IllegalArgumentException("${reporter.messages.size()} errors were found!")
}
} else {
if (logger.isEnabled(LogLevel.DEBUG)) {
logger.warn("Check task '{}' found {} errors and fixed {}:\n{}", name, reporter.messages.size(), reporter.fixed.size(), reporter.fixed.join('\n'))
} else {
logger.warn("Check task '{}' found {} errors and fixed {}.", name, reporter.messages.size(), reporter.fixed.size())
}
if (reporter.notFixed) {
logger.error('{} errors could not be fixed:\n{}', reporter.notFixed.size(), reporter.notFixed.join('\n'))
if (!ignoreFailures) {
throw new IllegalArgumentException("${reporter.notFixed.size()} errors which cannot be fixed were found!")
}
}
}
}
}
abstract void check(Reporter reporter, boolean fix)
@CompileStatic
static final class Reporter {
final boolean trackFixed
Reporter(boolean trackFixed) {
this.trackFixed = trackFixed
}
public final List<String> messages = []
public final List<String> fixed = []
public final List<String> notFixed = []
void report(String message, boolean canBeFixed = true) {
messages.add(message)
if (trackFixed) {
if (canBeFixed) {
fixed.add(message)
} else {
notFixed.add(message)
}
}
}
}
static <T extends CheckTask> void registerTask(TaskContainer tasks, String taskName, @DelegatesTo.Target('type') Class<T> clazz,
@DelegatesTo(genericTypeIndex = 0, target = 'type')
@ClosureParams(ThirdParam.FirstGenericType) Closure configuration) {
taskName = taskName.capitalize()
tasks.register("check$taskName", clazz) { CheckTask task ->
def castedTask = task as T
configuration.setDelegate(castedTask)
configuration.call(castedTask)
castedTask.mode.set(CheckMode.CHECK)
castedTask.group = 'checks'
}
tasks.named('checkAll').configure { it.dependsOn("check$taskName") }
tasks.register("checkAndFix$taskName", clazz) { CheckTask task ->
def castedTask = task as T
configuration.setDelegate(castedTask)
configuration.call(castedTask)
castedTask.mode.set(CheckMode.FIX)
castedTask.group = 'checks'
}
tasks.named('checkAllAndFix').configure { it.dependsOn("checkAndFix$taskName") }
}
}
+73
View File
@@ -0,0 +1,73 @@
plugins {
id 'java-library'
id 'net.minecraftforge.gradle.patcher'
}
evaluationDependsOn(':mcp')
repositories {
mavenCentral()
maven gradleutils.forgeMaven
maven gradleutils.minecraftLibsMaven
}
java.toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
dependencies {
implementation(libs.forgespi)
}
patcher {
parent = project(':mcp')
mcVersion = MC_VERSION
patchedSrc = file('src/main/java')
mappings channel: MAPPING_CHANNEL, version: MAPPING_VERSION
runs {
clean_client {
client true
taskName 'clean_client'
ideaModule "${rootProject.name}.${project.name}.main"
main 'net.minecraft.client.main.Main'
workingDirectory project.file('run/client')
args '--gameDir', '.'
args '--version', MC_VERSION
args '--assetsDir', downloadAssets.output
args '--assetIndex', '{asset_index}'
args '--accessToken', '0'
}
clean_server {
client false
taskName 'clean_server'
ideaModule "${rootProject.name}.${project.name}.main"
main 'net.minecraft.server.Main'
workingDirectory project.file('run/server')
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
options.warnings = false // Shutup deprecated for removal warnings
}
eclipse.classpath.file.whenMerged {
// Disable optional warnings on the minecraft decompiled source code. It just muddies the warning window and hides warnings in our own codebase
def src = entries.find { it.path == 'src/main/java' }
if (src == null) throw new IllegalStateException("You must run `setup` task before importing")
src.entryAttributes['ignore_optional_problems'] = 'true'
}
tasks.named('genPatches').configure {
onlyIf { false }
}
tasks.named('extractRangeMap').configure {
onlyIf { false }
tool = 'net.minecraftforge:Srg2Source:8.1.0:fatjar'
}
+1073
View File
File diff suppressed because it is too large. Load diff
+85
View File
@@ -0,0 +1,85 @@
import net.minecraftforge.forge.tasks.CleanProperties
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'net.minecraftforge.gradleutils'
group = 'net.minecraftforge'
version = VERSION
println("Version: $version")
repositories {
mavenCentral()
maven gradleutils.forgeMaven
maven gradleutils.minecraftLibsMaven
//mavenLocal()
}
tasks.withType(Javadoc).configureEach {
options.tags = [
'apiNote:a:<em>API Note:</em>',
'implSpec:a:<em>Implementation Requirements:</em>',
'implNote:a:<em>Implementation Note:</em>'
]
options.addStringOption('Xdoclint:all,-missing', '-public')
}
// We need to write the manifest to the binary file so we have properly versioned packaged at dev time.
tasks.register('writeManifest') {
doLast {
if (plugins.findPlugin('net.minecraftforge.gradle.patcher')) // Forge project
universalJar.manifest.writeTo(rootProject.file('src/main/resources/META-INF/MANIFEST.MF'))
else
jar.manifest.writeTo(project.file('src/main/resources/META-INF/MANIFEST.MF'))
}
}
tasks.register('generateResources') {
dependsOn('writeManifest')
}
// 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 += '-Xmx3G' // Needed to make compiling faster, and not run out of heap space in some cases.
}
// Merge the resources and classes into the same directory. We'll need to split them at runtime because
// Minecraft and Forge are in the same sourceSet as they are inter dependent.. for now..
sourceSets.each {
def dir = layout.buildDirectory.dir("classes/java/$it.name")
it.output.resourcesDir = dir
it.java.destinationDirectory = dir
}
tasks.register('copyEclipseSettings') {
doLast {
rootProject.fileTree('ide/eclipse/template/.settings/').matching { include '**/*.prefs' }.each { file ->
def target = project.file('.settings/' + file.name)
def temp = new CleanProperties().load(file)
def exst = new CleanProperties().load(target)
exst.put('eclipse.preferences.version', '1')
exst.putAll(temp)
exst.store(target)
}
}
}
// TODO: [Gradle][IntelliJ] Auto trigger these tasks on import.
eclipse {
// Run everytime eclipse builds the code
//autoBuildTasks writeManifest
// Run when importing the project
synchronizationTasks generateResources, copyEclipseSettings, eclipseClasspath, eclipseProject
}
idea {
module {
// IntelliJ IDEA does not do this by itself anymore...
downloadJavadoc = true
downloadSources = true
}
}
+2 -8
View File
@@ -2,8 +2,8 @@
MinecraftForge
=============
[![Stable Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Stable&prefix=1.21.11-&query=$.promos["1.21.11-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)][Download]
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=1.21.11-&query=$.promos["1.21.11-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
[![Stable Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Stable&prefix=1.21.1-&query=$.promos["1.21.1-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)][Download]
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=1.21.1-&query=$.promos["1.21.1-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
)][Download] [![Discord](https://img.shields.io/discord/313125603924639766.svg?color=%237289da&label=Discord&logo=discord&logoColor=%237289da)][Discord] [![Support](https://img.shields.io/badge/Patreon-Support-orange.svg?logo=Patreon)][Patreon]
Forge is a free, open-source modding API all of your favourite mods use!
@@ -70,11 +70,6 @@ Please ensure you have a valid email address associated with your GitHub account
to use and modify. However, it costs money to run such a large project as this, so please consider
[becoming a patron][Patreon].*
### Thanks
[![YourKit](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/)
YourKit for providing us accesss to their [YourKit Java Profiler] which helps identify bottlenecks and places for improvment.
[Contributing]: ./CONTRIBUTING.md
[CLA]: https://cla-assistant.io/MinecraftForge/MinecraftForge
@@ -98,4 +93,3 @@ YourKit for providing us accesss to their [YourKit Java Profiler] which helps id
[Pull-Requests]: https://docs.minecraftforge.net/en/latest/forgedev/#making-changes-and-pull-requests
[Patreon]: https://www.patreon.com/LexManos
[YourKit Java Profiler]: https://www.yourkit.com/java/profiler/
+24
View File
@@ -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
+39 -50
View File
@@ -1,78 +1,67 @@
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
id 'net.minecraftforge.forge.build.convention'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
gradleutils.displayName = 'FML'
final vendor = 'Forge Development LLC'
description = 'Modifications to Minecraft to enable mod developers.'
apply from: rootProject.file('build_shared.gradle')
dependencies {
compileOnly libs.jetbrains.annotations
compileOnly(libs.nulls)
api libs.eventbus
api(libs.eventbus)
implementation projects.fmlloader
implementation libs.commons.io
implementation(project(':fmlloader'))
implementation(libs.commons.io)
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
'FMLModType': 'LIBRARY'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FML',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML',
'Implementation-Version': '1.0',
'Implementation-Vendor': 'Forge Development LLC'
] as LinkedHashMap, 'net/minecraftforge/fml/')
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:-unchecked'
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
'FMLModType' : 'LIBRARY'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Version': '1.0',
'Implementation-Vendor' : vendor
], 'net/minecraftforge/fml/')
}
license {
header = rootProject.file('LICENSE-header.txt')
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlcore'
pom {
name = project.name
description = 'Modifactions to Minecraft to enable mod developers.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
@@ -5,7 +5,7 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.IEventBus;
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<BusGroup> getForgeBus() {
public static Supplier<IEventBus> getForgeBus() {
return PROVIDER.getForgeBusSupplier();
}
@@ -25,18 +25,7 @@ import java.util.function.Supplier;
* <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>
*
* @deprecated Historically, this allowed for safe execution of code on the correct side by exploiting a low-level
* detail in Java 8, but in newer versions a simple if check is enough.
* <br>
* For example:
* <pre>{@code
* if (FMLEnvironment.dist.isClient()) {
* // client code here
* }
* }</pre>
*/
@Deprecated(since = "1.21.5", forRemoval = true)
public final class DistExecutor
{
private static final Logger LOGGER = LogManager.getLogger();
@@ -5,13 +5,13 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.config.IConfigEvent;
import java.util.function.Supplier;
public interface IBindingsProvider {
Supplier<BusGroup> getForgeBusSupplier();
Supplier<IEventBus> getForgeBusSupplier();
Supplier<I18NParser> getMessageParser();
Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration();
}
@@ -5,6 +5,8 @@
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.Optional;
@@ -59,27 +61,27 @@ public interface IModLoadingState {
/**
* 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 Optional<CompletableFuture<Void>> buildTransition(
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar
) {
default <T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar) {
return buildTransition(syncExecutor, parallelExecutor, progressBar,
e -> CompletableFuture.completedFuture(null),
e -> CompletableFuture.completedFuture(null)
);
e -> CompletableFuture.runAsync(() -> {}, e),
e -> CompletableFuture.runAsync(() -> {}, e));
}
/**
* Builds the transition task for this state. The pre-sync and post-sync task functions allow the transition builder
* to run these tasks on the same executor as the actual event dispatch and pre/post hooks.
*
* @param <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
@@ -87,6 +89,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,
final Executor parallelExecutor,
final ProgressMeter progressBar,
@@ -5,9 +5,12 @@
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;
@@ -15,67 +18,103 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("unchecked")
public interface IModStateTransition {
/** Magic value to allow me to optimize the futures list by ignoring the default value without making old methods nullable. */
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> NULL_HOOK = (e, g) -> CompletableFuture.completedFuture(null);
static IModStateTransition buildNoopTransition() {
return ModStateTransitionHelper.NOOP;
return new NoopTransition();
}
default CompletableFuture<Void> build(
final String name,
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask
) {
return ModStateTransitionHelper.build(this, name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
default <T extends Event & IModBusEvent>
CompletableFuture<Void> build(final 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 BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextModLoadingStage() {
return ModLoadingStage::nextState;
}
/**
* This used to allow you to fire multiple events during the transition. However, in doing so it would cause issues with the default
* ModContainer's event handlers causing issues such as mod classes being constructed multiple times.
*/
@Deprecated(since = "1.21.3", forRemoval = true)
default Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
return () -> Stream.ofNullable(eventFunction());
}
@Nullable
default <T extends IModBusEvent> EventGenerator<T> eventFunction() {
return null;
private <T extends Event & IModBusEvent>
EventGenerator<T> addCompletableFutureTaskForModDispatch(final Executor syncExecutor,
final Executor parallelExecutor,
final List<CompletableFuture<Void>> completableFutures,
final 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;
}
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();
/**
* I think this was meant as a way to do some things for each mod container beforge/after the transition had been sent to the container.
* However, the Future returned by this was never linked to the main transition future in any way. Which means that it was run
* in parallel and couldn't guarantee the state of the ModContainer.
* <p>
* Plus all existing code that I could find returned a completedFuture, so I don't think anyone ever used this.
* <p>
* If I were to add this back it would be a CompletableFuture wrap(ModContainer, CompletableFuture)
* <p>
* Added magic value NULL_HOOK to allow me to optimize the futures list by ignoring the default value without making this method nullable.
*/
@Deprecated(since = "1.21.3", forRemoval = true)
default BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook() { return NULL_HOOK; }
@Deprecated(since = "1.21.3", forRemoval = true)
default BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook() { return NULL_HOOK; }
interface EventGenerator<T extends IModBusEvent> extends Function<ModContainer, T> {
static <FN extends IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
interface EventGenerator<T extends Event & IModBusEvent> extends Function<ModContainer, T> {
static <FN extends Event & IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
return fn::apply;
}
}
}
record NoopTransition() implements IModStateTransition {
@Override
public Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
return Stream::of;
}
@Override
public ThreadSelector threadSelector() {
return ThreadSelector.SYNC;
}
@Override
public BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator() {
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
}
@Override
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook() {
return (t, f)-> CompletableFuture.completedFuture(null);
}
@Override
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook() {
return (t, f)-> CompletableFuture.completedFuture(null);
}
}
@@ -5,19 +5,22 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.config.IConfigEvent;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.forgespi.language.IModInfo;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -30,8 +33,13 @@ 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;
@@ -40,14 +48,11 @@ public abstract class ModContainer {
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);
final Set<ModContainer> dependencies = new HashSet<>();
/**
* If you want to handle the event, override {@link #dispatchConfigEvent(IConfigEvent)}
*/
@Deprecated(since = "1.21.3", forRemoval = true)
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
public ModContainer(IModInfo info) {
public ModContainer(IModInfo info)
{
this.modId = info.getModId();
// TODO: Currently not reading namespace from configuration..
this.namespace = this.modId;
@@ -77,35 +82,58 @@ public abstract class ModContainer {
/**
* Errored container state, used for filtering. Does nothing.
*/
ModContainer() {
ModContainer()
{
this.modLoadingStage = ModLoadingStage.ERROR;
modId = "BROKEN";
namespace = "BROKEN";
modInfo = null;
}
/**
* @return the modid for this mod
*/
public final String getModId() {
public final String getModId()
{
return modId;
}
/**
* @return the resource prefix for the mod
*/
public final String getNamespace() {
public final String getNamespace()
{
return namespace;
}
/**
* @return The current loading stage for this mod
*/
public ModLoadingStage getCurrentState() {
public ModLoadingStage getCurrentState()
{
return modLoadingStage;
}
public IModInfo getModInfo() {
public static <T extends Event & IModBusEvent> CompletableFuture<Void> buildTransitionHandler(
final ModContainer target,
final IModStateTransition.EventGenerator<T> eventGenerator,
final 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()
{
return modInfo;
}
@@ -114,7 +142,8 @@ 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);
}
@@ -158,17 +187,9 @@ public abstract class ModContainer {
*/
public abstract Object getMod();
/**
* @return the mod bus group for this specific mod, or null if there is none.
* Leave null if you use {@link BusGroup#DEFAULT} for your mod lifecycle events.
*/
public BusGroup getModBusGroup() {
return null;
}
/**
* Accept an arbitrary event for processing by the mod. Probably posted to an event bus in the lower level container.
* @param e Event to accept
*/
protected <T extends IModBusEvent> void acceptEvent(T e) {}
protected <T extends Event & IModBusEvent> void acceptEvent(T e) {}
}
@@ -5,6 +5,9 @@
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.forgespi.language.IModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.ModFileScanData;
@@ -12,15 +15,27 @@ 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.util.Collections;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -30,25 +45,27 @@ 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 {
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, IModFileInfo> fileById;
private final Map<String, ModFileInfo> fileById;
private List<ModContainer> mods;
private Map<String, ModContainer> indexedMods;
private List<ModFileScanData> modFileScanData;
private List<ModContainer> sortedContainers;
private ModList(final List<ModFile> modFiles, final List<ModInfo> sortedList) {
var modFileInfos = this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).toList();
this.sortedList = sortedList.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());
}
this.fileById = Map.copyOf(byId);
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);
}
@@ -65,12 +82,12 @@ public class ModList {
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", ""));
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList) {
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList)
{
INSTANCE = new ModList(modFiles, sortedList);
return INSTANCE;
}
@@ -79,59 +96,117 @@ public class ModList {
return INSTANCE;
}
public List<IModFileInfo> getModFiles() {
public List<IModFileInfo> getModFiles()
{
return modFiles;
}
public IModFileInfo getModFileById(String modid) {
public IModFileInfo getModFileById(String modid)
{
return this.fileById.get(modid);
}
void setLoadedMods(final List<ModContainer> modContainers) {
<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);
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;
}
}
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()];
for (var future : futures) {
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.sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c->sortedList.indexOf(c.getModInfo()))).toList();
this.indexedMods = modContainers.stream().collect(Collectors.toMap(ModContainer::getModId, Function.identity()));
}
@SuppressWarnings("unchecked")
public <T> Optional<T> getModObjectById(String modId) {
public <T> Optional<T> getModObjectById(String modId)
{
return getModContainerById(modId).map(ModContainer::getMod).map(o -> (T) o);
}
public Optional<? extends ModContainer> getModContainerById(String modId) {
public Optional<? extends ModContainer> getModContainerById(String modId)
{
return Optional.ofNullable(this.indexedMods.get(modId));
}
public Optional<? extends ModContainer> getModContainerByObject(Object obj) {
public Optional<? extends ModContainer> getModContainerByObject(Object obj)
{
return mods.stream().filter(mc -> mc.getMod() == obj).findFirst();
}
public List<IModInfo> getMods() {
public List<IModInfo> getMods()
{
return this.sortedList;
}
public boolean isLoaded(String modTarget) {
public boolean isLoaded(String modTarget)
{
return this.indexedMods.containsKey(modTarget);
}
public int size() {
public int size()
{
return mods.size();
}
public List<ModFileScanData> getAllScanData() {
if (modFileScanData == null) {
public List<ModFileScanData> getAllScanData()
{
if (modFileScanData == null)
{
modFileScanData = this.sortedList.stream().
map(IModInfo::getOwningFile).
filter(Objects::nonNull).
map(IModFileInfo::getFile).
distinct().
map(IModFile::getScanResult).
toList();
collect(Collectors.toList());
}
return modFileScanData;
}
public void forEachModFile(Consumer<IModFile> fileConsumer) {
public void forEachModFile(Consumer<IModFile> fileConsumer)
{
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
}
@@ -147,11 +222,13 @@ public class ModList {
this.sortedContainers.forEach(containerConsumer);
}
public List<ModContainer> getLoadedMods() {
return this.sortedContainers;
}
public <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
return indexedMods.values().stream().map(function);
}
private static class UncaughtModLoadingException extends ModLoadingException {
public UncaughtModLoadingException(ModLoadingStage stage, Throwable originalException) {
super(null, stage, "fml.modloading.uncaughterror", originalException);
}
}
}
@@ -6,7 +6,8 @@
package net.minecraftforge.fml;
import com.google.common.collect.ImmutableList;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import com.google.common.collect.Streams;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
@@ -32,6 +33,8 @@ 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;
@@ -71,7 +74,8 @@ import static net.minecraftforge.fml.Logging.LOADING;
* and completes the mod loading sequence.</dd>
* </dl>
*/
public class ModLoader {
public class ModLoader
{
private static final Logger LOGGER = LogManager.getLogger();
private final LoadingModList loadingModList;
@@ -80,25 +84,27 @@ public class ModLoader {
private final List<ModLoadingWarning> loadingWarnings;
private final ModStateManager stateManager;
private static boolean loadingStateValid;
private final Consumer<String> statusConsumer = StartupNotificationManager.modLoaderConsumer().orElse(msg -> {});
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<Consumer<String>> statusConsumer = StartupNotificationManager.modLoaderConsumer();
private final Set<IModLoadingState> completedStates = new HashSet<>();
private ModList modList;
private ModLoader() {
var loadingModList = this.loadingModList = FMLLoader.getLoadingModList();
var loadingExceptions = this.loadingExceptions = loadingModList.getErrors().stream()
private ModLoader()
{
this.loadingModList = FMLLoader.getLoadingModList();
this.loadingExceptions = this.loadingModList.getErrors().stream()
.flatMap(ModLoadingException::fromEarlyException)
.collect(Collectors.toList());
this.loadingWarnings = loadingModList.getBrokenFiles().stream()
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());
if (loadingExceptions.isEmpty()) {
if (this.loadingExceptions.isEmpty()) {
this.erroredModInfos = Collections.emptySet();
} else {
this.erroredModInfos = Collections.newSetFromMap(new IdentityHashMap<>());
this.erroredModInfos.addAll(loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
this.erroredModInfos.addAll(this.loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
}
loadingModList.getModFiles().stream()
this.loadingModList.getModFiles().stream()
.filter(ModFileInfo::missingLicense)
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(this.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()))
@@ -113,16 +119,16 @@ public class ModLoader {
private static String computeLanguageList() {
return "\n" + FMLLoader.getLanguageLoadingProvider()
.applyForEach(lp -> lp.name() + '@' + lp.getClass().getPackage().getImplementationVersion())
.applyForEach(lp -> lp.name() + "@" + lp.getClass().getPackage().getImplementationVersion())
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
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",""));
}
@@ -150,22 +156,20 @@ public class ModLoader {
loadingModList.getMods());
if (!this.loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.getFirst());
statusConsumer.accept("ERROR DURING MOD LOADING");
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(loadingExceptions);
}
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(List::stream)
.filter(bound -> !ForgeFeature.testFeature(FMLEnvironment.dist, bound))
.toList();
if (!failedBounds.isEmpty()) {
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds);
statusConsumer.accept("ERROR DURING MOD LOADING");
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(failedBounds.stream()
@@ -173,42 +177,19 @@ public class ModLoader {
.toList());
}
final var modContainers = new HashMap<String, ModContainer>();
for (var file : loadingModList.getModFiles()) {
var containers = this.buildMods(file.getFile());
for (var container : containers)
modContainers.put(container.getModId(), container);
}
final List<ModContainer> modContainers = loadingModList.getModFiles().stream()
.map(ModFileInfo::getFile)
.map(this::buildMods)
.flatMap(List::stream)
.toList();
if (!loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.getFirst());
statusConsumer.accept("ERROR DURING MOD LOADING");
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(loadingExceptions);
}
// Gather all dependencies, so we can make sure they have finished their transition events before we fire ours
for (var mod : modContainers.values()) {
for (var dep : mod.getModInfo().getDependencies()) {
var target = modContainers.get(dep.getModId());
if (target == null)
continue;
switch (dep.getOrdering()) {
case AFTER:
mod.dependencies.add(target);
break;
case BEFORE:
target.dependencies.add(mod);
break;
default:
break;
}
}
}
modList.setLoadedMods(modContainers.values().stream().toList());
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(this.modList)).sum());
@@ -233,19 +214,7 @@ public class ModLoader {
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);
statusConsumer.accept("Mod loading complete - %d mods loaded".formatted(this.modList.size()));
statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())));
progress.complete();
}
@@ -267,19 +236,17 @@ public class ModLoader {
}
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);
var pb = StartupMessageManager.addProgressBar("State transition " +state.name()+" running", 0);
syncExecutor.drive(ticker);
transition.accept(this.modList);
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) {
while (!transition.isDone()) {
syncExecutor.drive(ticker);
}
try {
transition.join();
} catch (CompletionException e) {
@@ -289,38 +256,36 @@ public class ModLoader {
.anyMatch(obj -> !(obj instanceof ModLoadingException));
if (hasNotModLoadingEx) {
LOGGER.fatal("Encountered non-modloading exceptions!", e);
statusConsumer.accept("ERROR DURING MOD LOADING");
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
throw e;
}
final List<ModLoadingException> modLoadingExceptions = Arrays.stream(t.getSuppressed())
.filter(ModLoadingException.class::isInstance)
.map(ModLoadingException.class::cast)
.toList();
.collect(Collectors.toList());
LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size());
statusConsumer.accept("ERROR DURING MOD LOADING");
statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING"));
throw new LoadingFailedException(modLoadingExceptions);
}
}
private List<ModContainer> buildMods(final IModFile modFile) {
final Map<String, IModInfo> modInfoMap = new HashMap<>();
for (var mod : modFile.getModFileInfo().getMods())
modInfoMap.put(mod.getModId(), mod);
private List<ModContainer> buildMods(final IModFile modFile)
{
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader());
final List<ModContainer> containers = new ArrayList<>();
for (var entry : modFile.getScanResult().getTargets().entrySet()) {
var container = buildModContainerFromTOML(modFile, modInfoMap, entry.getKey(), entry.getValue());
if (container != null)
containers.add(container);
}
final List<ModContainer> containers = modFile.getScanResult().getTargets()
.entrySet()
.stream()
.map(e -> buildModContainerFromTOML(modFile, modInfoMap, e))
.filter(Objects::nonNull)
.toList();
if (containers.size() != modInfoMap.size()) {
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().toList();
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().toList();
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);
@@ -335,15 +300,15 @@ public class ModLoader {
loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath()));
}
// remove errored mod containers
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).toList();
}
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap,
final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader
) {
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
try {
final String modId = idToProviderEntry.getKey();
final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue();
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)
@@ -366,70 +331,56 @@ public class ModLoader {
return loadingStateValid;
}
/** @deprecated Use {@link #hasCompletedState(IModLoadingState)} instead */
@Deprecated(forRemoval = true)
public boolean hasCompletedState(final String stateName) {
IModLoadingState state = stateManager.findState(stateName);
return completedStates.contains(state);
}
public boolean hasCompletedState(IModLoadingState state) {
return completedStates.contains(state);
}
public static <T extends IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
public <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event generator to a broken mod state");
return;
}
for (var mod : ModList.get().getLoadedMods())
mod.acceptEvent(generator.apply(mod));
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(generator.apply(mc)));
}
public static <T extends IModBusEvent> void postEvent(T e) {
public <T extends Event & IModBusEvent> void postEvent(T e) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return;
}
for (var mod : ModList.get().getLoadedMods())
mod.acceptEvent(e);
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
}
public static <T extends IModBusEvent> T postEventWithReturn(T e) {
public <T extends Event & IModBusEvent> T postEventWithReturn(T e) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return e;
}
for (var mod : ModList.get().getLoadedMods())
mod.acceptEvent(e);
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
return e;
}
@SuppressWarnings("removal")
public static <T extends IModBusEvent> void postEventWrapContainerInModOrder(T event) {
postEventWithWrapInModOrder(event,
(mc, e) -> ModLoadingContext.get().setActiveContainer(mc),
(mc, e) -> ModLoadingContext.get().setActiveContainer(null)
);
public <T extends Event & IModBusEvent> void postEventWrapContainerInModOrder(T event) {
postEventWithWrapInModOrder(event, (mc, e) -> ModLoadingContext.get().setActiveContainer(mc), (mc, e) -> ModLoadingContext.get().setActiveContainer(null));
}
public static <T extends IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
public <T extends Event & IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
if (!loadingStateValid) {
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
return;
}
for (var mod : ModList.get().getLoadedMods()) {
pre.accept(mod, e);
mod.acceptEvent(e);
post.accept(mod, e);
}
ModList.get().forEachModInOrder(mc -> {
pre.accept(mc, e);
mc.acceptEvent(e);
post.accept(mc, e);
});
}
public List<ModLoadingWarning> getWarnings() {
public List<ModLoadingWarning> getWarnings()
{
return ImmutableList.copyOf(this.loadingWarnings);
}
public void addWarning(ModLoadingWarning warning) {
public void addWarning(ModLoadingWarning warning)
{
this.loadingWarnings.add(warning);
}
@@ -439,7 +390,7 @@ public class ModLoader {
return runningDataGen;
}
private static final class ErroredModContainer extends ModContainer {
private static class ErroredModContainer extends ModContainer {
public ErroredModContainer() {
super();
}
@@ -8,24 +8,23 @@ 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. 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 Use the context provided by your language loader in your mod's constructor
*/
@Deprecated(forRemoval = true, since="1.21.1")
public static ModLoadingContext get() {
@@ -5,9 +5,10 @@
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.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -25,23 +26,19 @@ 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 {
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 {
@Override
public 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
) {
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) {
var transition = this.transition.orElse(null);
return transition == null
? Optional.empty()
@@ -55,12 +52,9 @@ public record ModLoadingState(
* @param name the name of the state
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) {
return new ModLoadingState(name, previous, ml -> "", ml -> 0, phase, Optional.empty(), Optional.empty());
return new ModLoadingState(name, previous, ml -> "", f->0, phase, Optional.empty(), Optional.empty());
}
/**
@@ -71,13 +65,11 @@ public record ModLoadingState(
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
*
* @deprecated Use the builder
* @return a mod loading state with state transition information and a default message
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase,
final IModStateTransition transition) {
return of(name, phase).after(previous).withTransition(transition);
return new ModLoadingState(name, previous, ml -> "Processing transition " + name, ModList::size, phase, Optional.empty(), Optional.of(transition));
}
/**
@@ -89,14 +81,11 @@ public record ModLoadingState(
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
* @return a mod loading state with state transition information and a custom message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous,
final Function<ModList, String> message, final ModLoadingPhase phase,
final IModStateTransition transition) {
return of(name, phase).after(previous).message(message).withTransition(transition);
return new ModLoadingState(name, previous, message, ModList::size, phase, Optional.empty(), Optional.of(transition));
}
/**
@@ -108,75 +97,9 @@ public record ModLoadingState(
* @param phase the mod loading phase the state belongs to
* @param inline an optional runnable, which runs before starting the transition from this state to the next
* @return a mod loading state with an inline runnable and default message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase,
final Consumer<ModList> 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)
&& Objects.equals(this.previous, that.previous);
}
@Override
public int hashCode() {
int result = phase.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + Objects.hashCode(previous);
return result;
}
public static class Builder {
private final String name;
private final ModLoadingPhase phase;
private String after;
private Function<ModList, String> message = null;
private ToIntFunction<ModList> 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(ml -> value); }
public Builder message(final Function<ModList, String> value) {
this.message = value;
return this;
}
public Builder size(final int value) { return size(ml -> value); }
public Builder size(final ToIntFunction<ModList> value) {
this.size = value;
return this;
}
public ModLoadingState empty() {
return new ModLoadingState(name, after, message != null ? message : ml -> "", size != null ? size : ml -> 0, phase, Optional.empty(), Optional.empty());
}
public ModLoadingState withTransition(final IModStateTransition transition) {
return new ModLoadingState(name, after, message != null ? message : ml -> "Processing transition " + name, size != null ? size : ModList::size, phase, Optional.empty(), Optional.of(transition));
}
public ModLoadingState withInline(final Consumer<ModList> inline) {
return new ModLoadingState(name, after, message != null ? message : ml -> "Processing work " + name, size != null ? size : ml -> 0, phase, Optional.of(inline), Optional.empty());
}
return new ModLoadingState(name, previous, ml -> "Processing work " + name, ml->0, phase, Optional.of(inline), Optional.empty());
}
}
@@ -41,13 +41,11 @@ public class ModStateManager {
}
public IModLoadingState findState(final String stateName) {
var result = stateMap.values()
return stateMap.values()
.stream()
.flatMap(List::stream)
.filter(mls -> mls.name().equals(stateName))
.findFirst()
.orElse(null);
if (result != null) return result;
else throw new IllegalArgumentException("Unknown IModLoadingState: " + stateName);
.orElseThrow(() -> new IllegalArgumentException("Unknown IModLoadingState: " + stateName));
}
}
@@ -1,184 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import net.minecraftforge.fml.IModStateTransition.EventGenerator;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
@ApiStatus.Internal
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;
}
}
record FutureResult<V>(V value, Throwable exception){}
static <V> CompletableFuture<List<FutureResult<V>>> gather(Collection<? extends CompletableFuture<? extends V>> futures) {
var list = new ArrayList<FutureResult<V>>(futures.size());
var results = new CompletableFuture[futures.size()];
for (var future : futures) {
int i = list.size();
list.add(null);
@SuppressWarnings("unchecked") // Bypass a weird javac error with generic by forcing it to be the exact type we want.
var raw = ((CompletableFuture<V>)future);
results[i] = raw.whenComplete((result, exception) -> list.set(i, new FutureResult<>(result, exception)));
}
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
}
private static <T extends IModBusEvent> void addCompletableFutureTaskForModDispatch(
final IModStateTransition transition,
final Executor executor,
final List<CompletableFuture<Void>> completableFutures,
final ProgressMeter progressBar,
final EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState
) {
@SuppressWarnings("removal")
var preDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator);
if (preDispatchHook != null)
completableFutures.add(preDispatchHook);
var modFutures = new LinkedHashMap<String, CompletableFuture<Void>>();
for (var mod : ModList.get().getLoadedMods()) {
CompletableFuture<Void> parent = null;
if (mod.dependencies.isEmpty()) {
parent = CompletableFuture.allOf();
} else {
var deps = new CompletableFuture[mod.dependencies.size()];
int idx = 0;
for (var depContainer : mod.dependencies) {
var future = modFutures.get(depContainer.getModId());
if (future == null)
throw new IllegalStateException("Could not find dependency future " + depContainer.getModId() + " for " + mod.getModId());
deps[idx++] = future;
}
parent = CompletableFuture.allOf(deps);
}
@SuppressWarnings("removal")
var dispatch = parent
.thenRunAsync(() -> {
ModLoadingContext.get().setActiveContainer(mod);
var handler = mod.activityMap.get(mod.modLoadingStage);
if (handler != null)
handler.run();
mod.acceptEvent(eventGenerator.apply(mod));
}, executor)
.whenComplete((mc, exception) -> {
mod.modLoadingStage = nextState.apply(mod.modLoadingStage, exception);
progressBar.increment();
ModLoadingContext.get().setActiveContainer(null);
});
modFutures.put(mod.getModId(), dispatch);
}
var dispatch = gather(modFutures.values()).thenComposeAsync(ModStateTransitionHelper::completableFutureFromExceptionList, executor);
completableFutures.add(dispatch);
@SuppressWarnings("removal")
var postDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator);
if (postDispatchHook != null)
completableFutures.add(postDispatchHook);
}
private static <T extends IModBusEvent> CompletableFuture<Void> getHook(BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> hook, Executor executor, EventGenerator<T> eventGenerator) {
if (hook == null || hook == IModStateTransition.NULL_HOOK) return null;
@SuppressWarnings("unchecked")
var hookTyped = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>)hook;
return hookTyped.apply(executor, eventGenerator);
}
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
) {
List<CompletableFuture<Void>> futures = new ArrayList<>();
final var executor = transition.threadSelector().apply(syncExecutor, parallelExecutor);
@SuppressWarnings({ "removal", "unchecked" })
var events = transition.eventFunctionStream().get().map(f -> (EventGenerator<T>)f).toList();
for (int x = 0; x < events.size(); x++) {
var gen = events.get(x);
BiFunction<ModLoadingStage, Throwable, ModLoadingStage> state = x == events.size() - 1
? transition.nextModLoadingStage()
: ModLoadingStage::currentState;
addCompletableFutureTaskForModDispatch(transition, executor, futures, progressBar, gen, state);
}
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
final CompletableFuture<Void> eventDispatchCF = gather(futures).thenCompose(ModStateTransitionHelper::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 transition.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
}
}
@@ -5,7 +5,7 @@
package net.minecraftforge.fml.config;
import net.minecraftforge.eventbus.api.event.InheritableEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.Bindings;
import java.util.function.Function;
@@ -29,7 +29,7 @@ public interface IConfigEvent {
ModConfig getConfig();
@SuppressWarnings("unchecked")
default <T extends InheritableEvent & IConfigEvent> T self() {
default <T extends Event & IConfigEvent> T self() {
return (T) this;
}
}
@@ -5,33 +5,8 @@
package net.minecraftforge.fml.event;
import net.minecraftforge.eventbus.api.bus.BusGroup;
import net.minecraftforge.eventbus.api.bus.EventBus;
import net.minecraftforge.eventbus.api.event.InheritableEvent;
import org.jspecify.annotations.NonNull;
/**
* Marker interface for mod lifecycle events dispatched on the
* {@linkplain net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup() mod BusGroup} instead
* of the {@linkplain BusGroup#DEFAULT default BusGroup}.
*
* @apiNote Each mod gets its own unique mod BusGroup instance, obtained from the mod loading context. Most events
* should go on the default BusGroup.
* Marker interface for events dispatched on the ModLifecycle event bus instead of the primary event bus
*/
public interface IModBusEvent extends InheritableEvent {
/**
* @param modEventBusGroup Obtained from {@link net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup()} or your language provider's equivalent.
* @param eventClass The event type you want to get the associated bus for.
*/
static <T extends IModBusEvent> @NonNull EventBus<@NonNull T> getBus(@NonNull BusGroup modEventBusGroup, @NonNull Class<T> eventClass) {
// Do not copy! Temporary solution until the FML rewrite is complete. May throw in future
// without prior notice.
//
// Mods creating their own events should directly assign the result of the create method to a
// static final field in their event class, without wrapping or calling multiple times.
//
// Mods using existing events should instead use the existing static final BUS field in the
// event class (for most events) or getBus(BusGroup) method (for mod bus events).
return EventBus.create(modEventBusGroup, eventClass);
}
public interface IModBusEvent {
}
@@ -7,11 +7,8 @@ package net.minecraftforge.fml.util.thread;
import net.minecraftforge.fml.LogicalSide;
/**
* Attempts to determine the effect side of the current thread, based on know threads in networking.
* This is not guaranteed to be correct, and modders should use other methods to determine the logical side when possible.
*/
public class EffectiveSide {
public class EffectiveSide
{
public static LogicalSide get() {
final ThreadGroup group = Thread.currentThread().getThreadGroup();
return group instanceof SidedThreadGroup ? ((SidedThreadGroup) group).getSide() : LogicalSide.CLIENT;
@@ -7,13 +7,11 @@ package net.minecraftforge.fml.util.thread;
import net.minecraftforge.fml.LogicalSide;
public final class SidedThreadGroups {
public final class SidedThreadGroups
{
public static final SidedThreadGroup CLIENT = new SidedThreadGroup(LogicalSide.CLIENT);
public static final SidedThreadGroup SERVER = new SidedThreadGroup(LogicalSide.SERVER);
public static SidedThreadGroup get(boolean client) {
return client ? CLIENT : SERVER;
private SidedThreadGroups() {
}
private SidedThreadGroups() { }
}
+60 -85
View File
@@ -1,74 +1,67 @@
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.internal.os.OperatingSystem
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'jvm-test-suite'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
id 'net.minecraftforge.forge.build.convention'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
apply from: rootProject.file('build_shared.gradle')
import org.gradle.internal.os.OperatingSystem
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = "natives-windows"
break
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = "natives-windows"
break
}
gradleutils.displayName = 'FML Early Display'
final vendor = 'Forge Development LLC'
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
dependencies {
compileOnly libs.jetbrains.annotations
implementation projects.fmlloader
implementation projects.fmlcore
implementation libs.bundles.lwjgl
implementation earlyDisplayLibs.slf4j.api
implementation libs.jopt.simple
compileOnly(libs.nulls)
implementation(project(':fmlloader'))
implementation(project(':fmlcore'))
implementation(libs.bundles.lwjgl)
implementation('org.slf4j:slf4j-api:1.8.0-beta4')
implementation(libs.jopt.simple)
testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2')
testImplementation('org.powermock:powermock-core:2.0.9')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2')
testRuntimeOnly('org.slf4j:slf4j-jdk14:1.8.0-beta4')
testRuntimeOnly("org.lwjgl:lwjgl::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives")
testRuntimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives")
}
// See gradle/gradle#35070 (https://github.com/gradle/gradle/issues/35070)
// We are making the list of providers first BEFORE mapping them to a provider of the resolved objects so that they can be finalized after configuration.
var lwjglTestLibs = [
dependencies.variantOf(earlyDisplayTestLibs.lwjgl) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.glfw) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.opengl) { classifier lwjglNatives },
dependencies.variantOf(earlyDisplayTestLibs.lwjgl.stb) { classifier lwjglNatives }
].with { list ->
provider { list.collect { it.get() } }
tasks.named('test', Test).configure {
useJUnitPlatform()
}
testing.suites.named('test', JvmTestSuite) {
useJUnitJupiter('5.8.2')
dependencies {
implementation earlyDisplayTestLibs.powermock.core
runtimeOnly earlyDisplayLibs.slf4j.jdk14
runtimeOnly.bundle lwjglTestLibs
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer': 'boot'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FML Early Display',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML Early Display',
'Implementation-Vendor': 'Forge Development LLC',
'Implementation-Version': '1.0'
] as LinkedHashMap, 'net/minecraftforge/fml/earlydisplay/')
}
}
@@ -76,42 +69,24 @@ tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer' : 'boot'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Vendor' : vendor,
'Implementation-Version': '1.0'
], 'net/minecraftforge/fml/earlydisplay/')
}
license {
header = rootProject.file('LICENSE-header.txt')
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlearlydisplay'
pom {
name = project.name
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
@@ -85,17 +85,11 @@ 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 static final ScheduledExecutorService renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
final var thread = Executors.defaultThreadFactory().newThread(r);
thread.setName("EarlyDisplay");
thread.setDaemon(true);
return thread;
});
private ScheduledExecutorService renderScheduler;
private int fbWidth;
private int fbHeight;
private int fbScale;
@@ -154,7 +148,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 " + FMLLoader.versionInfo().forgeVersion()));
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + forgeVersion));
performanceInfo = new PerformanceInfo();
return start(mcVersion, forgeVersion);
}
@@ -243,7 +237,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.addFirst(RenderElement.squir());
this.elements.add(0, RenderElement.squir());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -263,11 +257,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (var itr = this.elements.iterator(); itr.hasNext(); ) {
var element = itr.next();
if (!element.render(context, framecount))
itr.remove();
}
this.elements.removeIf(element -> !element.render(context, framecount));
if (animationTimerTrigger.compareAndSet(true, false)) // we only increment the framecount on a periodic basis
framecount++;
}
@@ -292,6 +282,12 @@ 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;
@@ -303,7 +299,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
return this.glVersion;
}
private static void crashElegantly(String errorDetails) {
private void crashElegantly(String errorDetails) {
StringBuilder msgBuilder = new StringBuilder(2000);
msgBuilder.append("Failed to initialize graphics window with current settings.\n");
msgBuilder.append("\n\n");
@@ -492,6 +488,10 @@ 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;
@@ -511,7 +511,7 @@ public class DisplayWindow implements ImmediateWindowProvider {
this.winY = y;
}
}
private static void handleLastGLFWError(BiConsumer<Integer, String> handler) {
private void handleLastGLFWError(BiConsumer<Integer, String> handler) {
try (MemoryStack memorystack = MemoryStack.stackPush()) {
PointerBuffer pointerbuffer = memorystack.mallocPointer(1);
int error = glfwGetError(pointerbuffer);
@@ -639,7 +639,8 @@ public class DisplayWindow implements ImmediateWindowProvider {
}
public void addMojangTexture(final int textureId) {
this.elements.addFirst(RenderElement.mojang(textureId, framecount));
this.elements.add(0, RenderElement.mojang(textureId, framecount));
// this.elements.get(0).retire(framecount + 1);
}
public void close() {
@@ -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,9 +38,6 @@ 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> {}
@@ -59,18 +56,11 @@ 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;
@@ -85,7 +75,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 = Math.clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f);
final float fade = 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;
@@ -94,9 +84,19 @@ 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;
@@ -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) {
@@ -187,10 +187,10 @@ public class RenderElement {
}
private static float[] indeterminateBar(int frame, boolean isActive) {
if (RenderElement.globalAlpha != 0xFF || !isActive) {
return new float[] { 0f, 1f };
return new float[] {0f,1f};
} else {
int progress = frame % 100;
return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.clamp((progress + 2) / 100f, 0f, 1f) };
var progress = frame % 100;
return new float[]{clamp((progress - 2) / 100f, 0f, 1f), clamp((progress + 2) / 100f, 0f, 1f)};
}
}
@@ -249,8 +249,8 @@ public class RenderElement {
};
}
private static Renderer initializeText(SimpleFont font, TextGenerator textGenerator) {
return (bb, context, frame) -> renderText(font, textGenerator, bb, context);
private static Initializer initializeText(SimpleFont font, TextGenerator textGenerator) {
return () -> (bb, context, frame) -> renderText(font, textGenerator, bb, context);
}
private static void renderText(final SimpleFont font, final TextGenerator textGenerator, final SimpleBufferBuilder bb, final DisplayContext context) {
@@ -264,13 +264,14 @@ 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));
}
private static Renderer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
int[] imgSize = STBHelper.loadTextureFromClasspath(textureFileName, size, GL_TEXTURE0 + textureNumber + INDEX_TEXTURE_OFFSET);
return (bb, ctx, frame) -> {
ctx.elementShader().updateTextureUniform(textureNumber + INDEX_TEXTURE_OFFSET);
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
renderTexture(bb, ctx, frame, imgSize, positionAndColour);
private static 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);
};
};
}
@@ -280,13 +281,21 @@ public class RenderElement {
bb.draw();
}
@Deprecated(since = "1.21.5", forRemoval = true)
public static float clamp(float num, float min, float max) {
return Math.clamp(num, min, max);
if (num < min) {
return min;
} else {
return Math.min(num, max);
}
}
public static int clamp(int num, int min, int max) {
return Math.min(max, Math.max(num, min));
if (num < min) {
return min;
} else {
return Math.min(num, max);
}
}
public static int hsvToRGB(float hue, float saturation, float value) {
@@ -11,6 +11,8 @@ 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;
@@ -22,7 +24,7 @@ public class SimpleFont {
private final int textureNumber;
private final int lineSpacing;
private final int descent;
private static final int GLYPH_COUNT = 127 - 32;
private final int GLYPH_COUNT = 127-32;
private final Glyph[] glyphs;
private record Glyph(char c, int charwidth, int[] pos, float[] uv) {
@@ -120,7 +122,7 @@ public class SimpleFont {
case '\n', '\t' -> 0;
case ' ' -> glyphs[0].charwidth();
default -> {
if (c - 32 < GLYPH_COUNT && c > 32) {
if (c - 32 < this.GLYPH_COUNT && c > 32) {
yield this.glyphs[c - 32].charwidth();
} else {
yield 0;
@@ -151,7 +153,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 < GLYPH_COUNT && c > 32) {
if (c-32 < font.GLYPH_COUNT && c > 32) {
pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb);
}
yield pos;
+77 -160
View File
@@ -1,74 +1,67 @@
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import net.minecraftforge.forgedev.legacy.tasks.JarJarMetadataOptions
import org.gradle.api.plugins.jvm.JvmTestSuite
import javax.inject.Inject
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'jvm-test-suite'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
alias libs.plugins.apt
id 'net.minecraftforge.forge.build.convention'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
gradleutils.displayName = 'FMLLoader'
final vendor = 'Forge Development LLC'
description = 'Modifications to Minecraft to enable mod developers.'
apply from: rootProject.file('build_shared.gradle')
configurations.forEach{ it.transitive = false }
dependencies {
compileOnly(libs.nulls)
api(libs.bundles.asm) // Needed by all the black magic
api(libs.forgespi)
api(libs.mergetool.api)
api(libs.log4j.api)
api(libs.slf4j.api)
api(libs.guava)
api(libs.gson)
api(libs.maven.artifact)
api(libs.apache.commons)
api(libs.bundles.night.config)
api(libs.modlauncher)
api(libs.coremods)
api(libs.mojang.logging)
api(libs.jarjar.selector)
api(libs.jarjar.meta)
implementation(libs.jopt.simple)
implementation(libs.securemodules)
implementation(libs.accesstransformers)
implementation(libs.terminalconsoleappender)
implementation(libs.jimfs)
implementation(libs.log4j.core)
annotationProcessor(libs.bundles.log4j)
testCompileOnly(libs.nulls)
testRuntimeOnly(libs.bootstrap)
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
dependencies {
compileOnly libs.jetbrains.annotations
// TODO [FMLLoader] Figure out a better way to lock transitive dependencies to the explicit versions shipped by the isntaller
api(libs.bundles.asm ){ transitive = false } // Needed by all the black magic
api(libs.forgespi ){ transitive = false }
api(libs.mergetool.api ){ transitive = false }
api(libs.log4j.api ){ transitive = false }
api(libs.slf4j.api ){ transitive = false }
api(libs.guava ){ transitive = false }
api(libs.gson ){ transitive = false }
api(libs.maven.artifact ){ transitive = false }
api(libs.apache.commons ){ transitive = false }
api(libs.bundles.night.config){ transitive = false }
api(libs.modlauncher ){ transitive = false }
api(libs.mojang.logging ){ transitive = false }
api(libs.jarjar.selector ){ transitive = false }
api(libs.jarjar.meta ){ transitive = false }
implementation(libs.jopt.simple ){ transitive = false }
implementation(libs.securemodules ){ transitive = false }
implementation(libs.accesstransformers ){ transitive = false }
implementation(libs.terminalconsoleappender){ transitive = false }
implementation(libs.jimfs ){ transitive = false }
// 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 ){ transitive = false }
annotationProcessor(libs.bundles.log4j){ transitive = false }
}
testing.suites.named('test', JvmTestSuite) {
dependencies {
compileOnly libs.jetbrains.annotations
runtimeOnly libs.bootstrap
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
'Forge-Module-Layer': 'boot'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FMLLoader',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FMLLoader',
'Implementation-Vendor': 'Forge Development LLC',
'Implementation-Version': FORGE_VERSION
] as LinkedHashMap, 'net/minecraftforge/fml/loading/')
}
}
@@ -76,114 +69,38 @@ tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
'Forge-Module-Layer' : 'boot'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Vendor' : vendor,
'Implementation-Version': forgeVersion
], 'net/minecraftforge/fml/loading/')
}
license {
header = rootProject.file('LICENSE-header.txt')
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlloader'
pom {
name = project.name
description = 'Modifactions to Minecraft to enable mod developers.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom {
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
tasks.register('writeForgeVersionJson') {
doLast {
file('src/main/resources/forge_version.json').json = [
forge: FORGE_VERSION,
mc: MC_VERSION,
mcp: MCP_VERSION
]
}
}
// A simple task to write JSON to a file. Using Task#doLast to output files is no longer supported.
// This code may seem verbose, but it's better this way. If more projects need this, we can move it to buildSrc.
@CompileStatic
@PackageScope abstract class WriteForgeVersionJson extends DefaultTask {
abstract @Input Property<String> getForgeVersion()
abstract @Input Property<String> getMinecraftVersion()
abstract @Input Property<String> getMcpVersion()
abstract @OutputFile RegularFileProperty getOutputFile()
@Inject
WriteForgeVersionJson() {}
@TaskAction
@CompileDynamic
void exec() {
var json = new groovy.json.JsonBuilder()
json {
forge this.forgeVersion.get()
mc this.minecraftVersion.get()
mcp this.mcpVersion.get()
}
this.outputFile.asFile.get().text = json.toPrettyString()
}
}
tasks.register('writeForgeVersionJson', WriteForgeVersionJson) {
forgeVersion = project.forgeVersion
minecraftVersion = project.minecraftVersion
mcpVersion = project.mcpVersion
outputFile = file('src/main/resources/forge_version.json')
}
tasks.register('jarJarOptionsJson', 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') {
dependsOn(
tasks.named('eclipseJdt'),
tasks.named('eclipseJdtApt'),
tasks.named('eclipseFactorypath'),
tasks.named('writeForgeVersionJson', WriteForgeVersionJson),
tasks.named('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'
}
}
tasks.named('generateResources').configure {
dependsOn('writeForgeVersionJson')
}
@@ -34,10 +34,11 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
* }</code>
* </pre>
*/
public final class CapabilityTokenSubclass implements ILaunchPluginService {
public 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() {
@@ -55,11 +56,8 @@ public final class CapabilityTokenSubclass implements ILaunchPluginService {
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraft/") || internalName.startsWith("com/mojang/"))
return NAY;
if (internalName.startsWith("net/minecraftforge/common/capabilities/"))
return YAY;
return internalName.contains("$") ? YAY : NAY; // Anonymous subclasses only
return YAY;
}
@Override
@@ -72,7 +70,7 @@ public final class CapabilityTokenSubclass implements ILaunchPluginService {
}
return ComputeFlags.SIMPLE_REWRITE;
} else if (CAP_INJECT.equals(classNode.superName)) {
var cls = new Object() { String value = null; };
Holder cls = new Holder();
SignatureReader reader = new SignatureReader(classNode.signature); // Having a node version of this would probably be useful.
reader.accept(new SignatureVisitor(Opcodes.ASM9) {
@@ -108,4 +106,8 @@ public final class CapabilityTokenSubclass implements ILaunchPluginService {
return ComputeFlags.NO_REWRITE;
}
}
private static final class Holder {
String value;
}
}
@@ -0,0 +1,131 @@
/*
* 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.toUnmodifiableMap(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 static 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,21 +47,16 @@ 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.get().getMods().size() > 2; // 2: forge, minecraft
}
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty || !LazyInit.ANY_MODS)
if (isEmpty)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraft/client/model") || internalName.startsWith("net/minecraft/world"))
return YAY;
if (internalName.startsWith("net/minecraftforge/") || internalName.startsWith("com/mojang/"))
return NAY;
return NAY;
return YAY;
}
@Override
@@ -6,13 +6,14 @@
package net.minecraftforge.fml.loading;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class BackgroundWaiter {
private static 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 {
@@ -16,6 +16,7 @@ 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.slf4j.Logger;
import java.nio.file.Path;
@@ -33,6 +34,7 @@ import static net.minecraftforge.fml.loading.LogMarkers.SCAN;
public class FMLLoader {
private static final Logger LOGGER = LogUtils.getLogger();
private static AccessTransformerService accessTransformer;
private static ICoreModProvider coreModProvider;
private static LanguageLoadingProvider languageLoadingProvider;
private static Dist dist;
private static String naming;
@@ -52,9 +54,15 @@ 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 {
@@ -92,7 +100,6 @@ 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(); ) {
@@ -114,6 +121,12 @@ 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);
@@ -143,8 +156,9 @@ public class FMLLoader {
}
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments) {
LOGGER.debug(SCAN, "Scanning for Mod Locators");
modValidator = ModDiscoverer.discoverMods(arguments);
LOGGER.debug(SCAN,"Scanning for Mod Locators");
var modDiscoverer = new ModDiscoverer(arguments);
modValidator = modDiscoverer.discoverMods();
var pluginResources = modValidator.getPluginResources();
return List.of(pluginResources);
}
@@ -157,6 +171,10 @@ public class FMLLoader {
return List.of(modValidator.getModResources());
}
public static ICoreModProvider getCoreModProvider() {
return coreModProvider;
}
public static LanguageLoadingProvider getLanguageLoadingProvider() {
return languageLoadingProvider;
}
@@ -7,6 +7,7 @@ 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;
@@ -18,6 +19,7 @@ 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 {
@@ -68,6 +70,7 @@ public class FMLServiceProvider implements ITransformationService {
@SuppressWarnings("rawtypes")
@Override
public @NotNull List<ITransformer> transformers() {
return List.of();
LOGGER.debug(CORE, "Loading coremod transformers");
return new ArrayList<>(FMLLoader.getCoreModProvider().getCoreModTransformers());
}
}
@@ -5,30 +5,33 @@
package net.minecraftforge.fml.loading;
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
import com.mojang.logging.LogUtils;
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.slf4j.Logger;
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;
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>
*/
public class LoadingModList {
public class LoadingModList
{
private static final Logger LOGGER = LogUtils.getLogger();
private static LoadingModList INSTANCE;
private final List<ModFileInfo> modFiles;
@@ -37,12 +40,15 @@ public class LoadingModList {
private final List<EarlyLoadingException> preLoadErrors;
private List<IModFile> brokenFiles;
private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> sortedList) {
private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> sortedList)
{
this.modFiles = modFiles.stream()
.map(ModFile::getModFileInfo)
.map(ModFileInfo.class::cast)
.toList();
this.sortedList = sortedList.stream().toList();
.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)
@@ -51,48 +57,63 @@ public class LoadingModList {
this.preLoadErrors = new ArrayList<>();
}
public static LoadingModList of(List<ModFile> modFiles, List<ModInfo> sortedList, final EarlyLoadingException earlyLoadingException) {
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;
}
public static LoadingModList get() {
return INSTANCE;
}
public void addAccessTransformers() {
var errors = new ArrayList<ExceptionData>();
for (ModFileInfo modFile : modFiles) {
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())
preLoadErrors.add(new EarlyLoadingException("Invalid Access Transformers", null, errors));
public void addCoreMods()
{
modFiles.stream()
.map(ModFileInfo::getFile)
.map(ModFile::getCoreMods)
.flatMap(List::stream)
.forEach(FMLLoader.getCoreModProvider()::addCoreMod);
}
public void addForScanning(BackgroundScanHandler backgroundScanHandler) {
public void addAccessTransformers()
{
var errors = new ArrayList<EarlyLoadingException.ExceptionData>();
for (ModFileInfo modFile : modFiles) {
ModFile mod = modFile.getFile();
for (Path at : mod.getAccessTransformers()) {
if (!Files.exists(at)) {
var message = "Invalid mod file: " + modFile.getFile().getFileName() + ". Missing Access Transformer: " + at;
errors.add(new EarlyLoadingException.ExceptionData(message));
LOGGER.error(message);
} else {
FMLLoader.addAccessTransformer(at, mod);
}
}
}
if (!errors.isEmpty()) {
preLoadErrors.add(new EarlyLoadingException("Invalid Access Transformers", null, errors));
}
}
public void addForScanning(BackgroundScanHandler backgroundScanHandler)
{
backgroundScanHandler.setLoadingModList(this);
modFiles.stream()
.map(ModFileInfo::getFile)
.forEach(backgroundScanHandler::submitForScanning);
}
public List<ModFileInfo> getModFiles() {
public List<ModFileInfo> getModFiles()
{
return modFiles;
}
public Path findResource(final String className) {
public Path findResource(final String className)
{
for (ModFileInfo mf : modFiles) {
final Path resource = mf.getFile().findResource(className);
if (Files.exists(resource)) return resource;
@@ -100,11 +121,55 @@ public class LoadingModList {
return null;
}
public ModFileInfo getModFileById(String modid) {
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;
}
};
}
public ModFileInfo getModFileById(String modid)
{
return this.fileById.get(modid);
}
public List<ModInfo> getMods() {
public List<ModInfo> getMods()
{
return this.sortedList;
}
@@ -7,10 +7,8 @@ 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;
@@ -42,17 +40,6 @@ 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.ModVersion;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
@@ -20,250 +20,235 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import 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 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 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;
private ModSorter(final List<ModFile> modFiles)
{
this.uniqueModListBuilder = new UniqueModListBuilder(modFiles);
}
public static LoadingModList sort(List<ModFile> mods, final List<ExceptionData> errors)
{
final ModSorter ms = new ModSorter(mods);
try {
modFiles = UniqueModListBuilder.buildUniqueList(mods).modFiles();
ms.buildUniqueList();
} 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(systemMods.files(), systemMods.mods(), e);
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).toList(), 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(verifyDependencyVersions(modFiles).stream(), errors.stream()).toList();
final List<ExceptionData> failedList = Stream.concat(ms.verifyDependencyVersions().stream(), errors.stream()).toList();
// if we miss one or the other, we abort now
if (!failedList.isEmpty()) {
return LoadingModList.of(systemMods.files(), systemMods.mods(), new EarlyLoadingException("failure to validate mod list", null, failedList));
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).toList(), new EarlyLoadingException("failure to validate mod list", null, failedList));
} else {
// Otherwise, lets try and sort the modlist and proceed
EarlyLoadingException earlyLoadingException = null;
try {
var sorted = sort(modFiles, named);
return LoadingModList.of(sorted.files(), sorted.mods(), null);
ms.sort();
} catch (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 LoadingModList.of(systemMods.files(), systemMods.mods(), e);
earlyLoadingException = e;
}
return LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException);
}
}
private static State sort(final List<ModFile> modFiles, final Map<String, ModInfo> named) {
final MutableGraph<ModInfo> graph = GraphBuilder.directed().build();
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)
.flatMap(List::stream)
.map(IModInfo::getDependencies)
.<IModInfo.ModVersion>flatMap(List::stream)
.forEach(dep -> addDependency(graph, dep));
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);
}
}
}
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 {
final List<ModFileInfo> 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');
}
}
catch (CyclePresentException e)
{
Set<Set<ModFileInfo>> cycles = e.getCycles();
if (LOGGER.isErrorEnabled(LOADING))
{
LOGGER.error(LOADING, "Mod Sorting failed.\nDetected Cycles: {}\n", cycles);
}
LOGGER.error(LOADING, buf.toString());
var dataList = cycles.stream()
.flatMap(Set::stream)
.<IModInfo>mapMulti((mf,c)->mf.getMods().forEach(c))
.map(IModInfo::getModId)
.map(list -> new ExceptionData("fml.modloading.cycle", list))
.toList();
throw new EarlyLoadingException("Sorting error", e, dataList);
}
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);
this.sortedList = sorted.stream()
.map(ModFileInfo::getMods)
.flatMap(List::stream)
.map(ModInfo.class::cast)
.collect(toList());
this.modFiles = sorted.stream()
.map(ModFileInfo::getFile)
.collect(toList());
}
private static State detectSystemMods(final List<ModFile> modFiles) {
private void addDependency(MutableGraph<ModFileInfo> topoGraph, IModInfo.ModVersion dep)
{
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;
final ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile();
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 -> {}
}
}
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) {
// 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);
var mods = new ArrayList<ModInfo>();
var files = new ArrayList<ModFile>();
this.systemMods = new ArrayList<>();
for (var systemMod : systemMods) {
var mod = findMod(modFiles, systemMod);
if (mod == null)
var container = modFilesByFirstId.get(systemMod);
if (container != null && !container.isEmpty()) {
LOGGER.debug("Found system mod: {}", systemMod);
this.systemMods.add(container.getFirst());
} else {
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 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;
}
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;
private List<EarlyLoadingException.ExceptionData> verifyDependencyVersions()
{
final var modVersions = modFiles.stream()
.map(ModFile::getModInfos)
.flatMap(List::stream)
.collect(toMap(IModInfo::getModId, IModInfo::getVersion));
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 var modVersionDependencies = modFiles.stream()
.map(ModFile::getModInfos)
.flatMap(List::stream)
.collect(groupingBy(Function.identity(), flatMapping(e -> e.getDependencies().stream(), toList())));
final var modRequirements = modVersionDependencies.values().stream()
.<IModInfo.ModVersion>flatMap(List::stream)
.filter(mv -> mv.getSide().isCorrectSide())
.collect(toSet());
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())) && !modVersionContained(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);
final var missingMandatory = new HashSet<ModVersion>();
final var missingOptional = new HashSet<ModVersion>();
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"))
);
}
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 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();
}
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;
return Collections.emptyList();
}
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(
private static String formatDependencyError(IModInfo.ModVersion dependency, Map<String, ArtifactVersion> modVersions)
{
ArtifactVersion installed = modVersions.get(dependency.getModId());
return String.format(
"\tMod ID: '%s', Requested by: '%s', Expected range: '%s', Actual version: '%s'",
dep.getModId(),
dep.getOwner().getModId(),
dep.getVersionRange(),
dependency.getModId(),
dependency.getOwner().getModId(),
dependency.getVersionRange(),
installed != null ? installed.toString() : "[MISSING]"
));
}
return String.join("\n", ret);
);
}
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));
private static boolean modVersionContained(IModInfo.ModVersion mv, Map<String, ArtifactVersion> modVersions) {
var modId = mv.getModId();
var range = mv.getVersionRange();
if (modVersions.containsKey(modId)
&& (range.containsVersion(modVersions.get(modId)) || modVersions.get(modId).toString().equals("0.0NONE")))
return true;
return VersionSupportMatrix.testVersionSupportMatrix(mv.getVersionRange(), mv.getModId(), "mod");
}
}
@@ -40,54 +40,20 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
return "runtimedistcleaner";
}
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) {
// TODO: [FML][RuntimeDistCleaner] In 26.1, only return YAY for MC classes on mod dev server
if (isEmpty)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraftforge/"))
return NAY;
// 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;
}
@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);
final class LazyInit {
private LazyInit() {}
private static final boolean CAN_EXPLODE = !FMLEnvironment.production && "21.6".equals(FMLLoader.versionInfo().mcVersion());
}
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);
throw new RuntimeException("Attempted to load class "+ classNode.name + " for invalid dist "+ DIST);
}
if (!annotations.isEmpty()) {
if (isNonMinecraftClass) {
if (LazyInit.CAN_EXPLODE)
throw new UnsupportedOperationException("Mod class " + classNode.name + " is annotated with @OnlyIn, this is no longer supported as it slowed down startup times");
else
LOGGER.warn(DISTXFORM, "Class {} is annotated with @OnlyIn, this is deprecated and won't work in a future MC release.", classNode.name);
}
if (!FMLEnvironment.production && hasModAnnotation(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 (!FMLEnvironment.production && !annotations.isEmpty() && hasModAnnotation(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 && !classNode.interfaces.isEmpty()) {
@@ -96,7 +62,7 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
continue;
if (classNode.interfaces.remove(ann.intf)) {
LOGGER.debug(DISTXFORM, "Removing Interface: {} implements {}", classNode.name, ann.intf);
LOGGER.debug(DISTXFORM,"Removing Interface: {} implements {}", classNode.name, ann.intf);
changed = true;
}
}
@@ -120,13 +86,6 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
LOGGER.debug(DISTXFORM, "Removing field: {}.{}", classNode.name, field.name);
itr.remove();
changed = true;
if (isNonMinecraftClass) {
if (LazyInit.CAN_EXPLODE)
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");
else
LOGGER.warn(DISTXFORM, "Field {} in class {} is annotated with @OnlyIn, this is deprecated and won't work in a future MC release.", field.name, classNode.name);
}
}
}
@@ -138,13 +97,6 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
itr.remove();
lambdaGatherer.accept(method);
changed = true;
if (isNonMinecraftClass) {
if (LazyInit.CAN_EXPLODE)
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");
else
LOGGER.warn(DISTXFORM, "Method {} in class {} is annotated with @OnlyIn, this is deprecated and won't work in a future MC release.", method.name, classNode.name);
}
}
}
@@ -171,7 +123,7 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
return changed ? ComputeFlags.SIMPLE_REWRITE : ComputeFlags.NO_REWRITE;
}
private record Target(String side, String intf) {
private static 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]
@@ -237,10 +189,6 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
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() {
@@ -250,6 +198,25 @@ public final class RuntimeDistCleaner implements ILaunchPluginService {
};
}
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;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraftforge/"))
return NAY;
// No Vanilla classes use @OnlyIn(Dist.SERVER), so we can skip them entirely when we're running on a client
if (FMLEnvironment.dist.isClient() && (internalName.startsWith("net/minecraft/") || internalName.startsWith("com/mojang/")))
return NAY;
return YAY;
}
private static class LambdaGatherer extends MethodVisitor {
private static final Handle META_FACTORY = new Handle(Opcodes.H_INVOKESTATIC,
"java/lang/invoke/LambdaMetafactory", "metafactory",
@@ -9,7 +9,6 @@ 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;
@@ -17,19 +16,26 @@ 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;
@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() {}
public class UniqueModListBuilder
{
private final static Logger LOGGER = LogUtils.getLogger();
private static final Logger LOGGER = LogUtils.getLogger();
private final List<ModFile> modFiles;
public UniqueModListBuilder(final List<ModFile> modFiles) {this.modFiles = modFiles;}
public UniqueModListData buildUniqueList()
{
List<ModFile> uniqueModList;
List<ModFile> uniqueLibListWithVersion;
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)
@@ -40,12 +46,12 @@ public final class UniqueModListBuilder {
.collect(groupingBy(UniqueModListBuilder::getModId));
// Select the newest by artifact version sorting of non-unique files thus identified
final List<ModFile> uniqueModList = modFilesByFirstId.entrySet().stream()
uniqueModList = modFilesByFirstId.entrySet().stream()
.map(UniqueModListBuilder::selectNewestModInfo)
.toList();
// Select the newest by artifact version sorting of non-unique files thus identified
final List<ModFile> uniqueLibListWithVersion = libFilesWithVersionByModuleName.entrySet().stream()
uniqueLibListWithVersion = libFilesWithVersionByModuleName.entrySet().stream()
.map(UniqueModListBuilder::selectNewestModInfo)
.toList();
@@ -57,10 +63,12 @@ public final class UniqueModListBuilder {
.collect(groupingBy(IModInfo::getModId));
// Transform to the full lib id list
final List<List<ModFile>> versionedLibs = uniqueLibListWithVersion.stream()
final Map<String, List<ModFile>> versionedLibIds = uniqueLibListWithVersion.stream()
.map(UniqueModListBuilder::getModId)
.map(libFilesWithVersionByModuleName::get)
.toList();
.collect(Collectors.toMap(
Function.identity(),
libFilesWithVersionByModuleName::get
));
// Its theoretically possible that some mod has somehow moved an id to a secondary place, thus causing a dupe.
// We can't handle this
@@ -73,24 +81,24 @@ public final class UniqueModListBuilder {
)).toList();
if (!dupedModErrors.isEmpty()) {
LOGGER.error(LOADING, "Found duplicate mods:\n{}", String.join("\n", dupedModErrors));
LOGGER.error(LOADING, "Found duplicate mods:\n{}", dupedModErrors.stream().collect(joining("\n")));
throw new EarlyLoadingException("Duplicate mods found", null, dupedModErrors.stream()
.map(EarlyLoadingException.ExceptionData::new)
.map(s -> new EarlyLoadingException.ExceptionData(s))
.toList());
}
final List<String> dupedLibErrors = versionedLibs.stream()
.filter(libModFiles -> libModFiles.size() > 1)
final List<String> dupedLibErrors = versionedLibIds.values().stream()
.filter(modFiles -> modFiles.size() > 1)
.map(mods -> String.format("\tLibrary: '%s' from files: %s",
getModId(mods.getFirst()),
mods.stream().map(ModFile::getFileName).collect(joining(", "))
)).toList();
if (!dupedLibErrors.isEmpty()) {
LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", String.join("\n", dupedLibErrors));
LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", dupedLibErrors.stream().collect(joining("\n")));
throw new EarlyLoadingException("Duplicate plugins or libraries found", null, dupedLibErrors.stream()
.map(EarlyLoadingException.ExceptionData::new)
.map(s -> new EarlyLoadingException.ExceptionData(s))
.toList());
}
@@ -111,7 +119,7 @@ public final class UniqueModListBuilder {
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.getFirst();
return modInfoList.get(0);
}
private static ArtifactVersion getVersion(final ModFile mf) {
@@ -119,7 +127,7 @@ public final class UniqueModListBuilder {
return mf.getJarVersion();
}
return mf.getModInfos().getFirst().getVersion();
return mf.getModInfos().get(0).getVersion();
}
private static String getModId(ModFile modFile) {
@@ -127,8 +135,9 @@ public final class UniqueModListBuilder {
return modFile.getSecureJar().name();
}
return modFile.getModFileInfo().getMods().getFirst().getModId();
return modFile.getModFileInfo().getMods().get(0).getModId();
}
public record UniqueModListData(List<ModFile> modFiles, Map<String, List<ModFile>> modFilesByFirstId) {}
}
@@ -18,7 +18,9 @@ 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.
@@ -54,16 +54,12 @@ public abstract class AbstractModProvider implements IModProvider {
@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();
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()));
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Failed to create secure jar for \"" + path + "\" - " + t.getMessage()));
}
IModFile mod;
@@ -72,7 +68,7 @@ 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, debug);
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, path);
mod = new ModFile(sj, this, ModFileParser::modsTomlParser);
// ModJarMetadata is only used when loading mods.toml mods as ModJarMetadata
mjm.setModFile(mod);
@@ -81,17 +77,17 @@ public abstract class AbstractModProvider implements IModProvider {
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, debug);
LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", JarFile.MANIFEST_NAME, type, path);
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 " + debug));
return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Invalid mod file found " + path));
return new IModLocator.ModFileOrException(mod, null);
}
protected static JarMetadata loadMetaFromJar(SecureJar jar, ModJarMetadata mjm) {
private static JarMetadata loadMetaFromJar(SecureJar jar, ModJarMetadata mjm) {
var info = jar.moduleDataProvider().open(MODULE_INFO).orElse(null);
if (info != null) {
try {
@@ -149,12 +145,6 @@ public abstract class AbstractModProvider implements IModProvider {
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();
@@ -74,8 +74,7 @@ public final class ClasspathLocator extends AbstractModProvider implements IModL
var ret = new ArrayList<ModFileOrException>();
for (var path : claimed) {
// Filter out anything found by the ServiceLoader
if (!ModDirTransformerDiscoverer.isServiceProvider(path)
&& !JarInJarDependencyLocator.isOption(path))
if (!ModDirTransformerDiscoverer.isServiceProvider(path))
ret.add(createMod(path));
}
return ret;
@@ -108,7 +107,7 @@ public final class ClasspathLocator extends AbstractModProvider implements IModL
return Path.of(URI.create(str));
}
static Path getPathFromResource(ClassLoader cl, String resource) {
private static Path getPathFromResource(ClassLoader cl, String resource) {
var url = cl.getResource(resource);
if (url == null)
return null;
@@ -0,0 +1,53 @@
/*
* 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 final 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() + "}";
}
}
@@ -9,6 +9,7 @@ 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;
@@ -5,39 +5,22 @@
package net.minecraftforge.fml.loading.moddiscovery;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import com.electronwill.nightconfig.core.file.FileConfig;
import com.google.common.collect.ImmutableMap;
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.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.forgespi.locating.ModFileLoadingException;
import net.minecraftforge.jarjar.selection.JarSelector;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
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;
@@ -45,20 +28,14 @@ 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.StringJoiner;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.Optional;
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";
@@ -66,238 +43,32 @@ 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) {
var mods = new ArrayList<IModFile>();
loadedMods.forEach(mods::add);
final List<IModFile> sources = new ArrayList<>();
loadedMods.forEach(sources::add);
var selector = new Selector(mods);
var dependenciesToLoad = JarSelector.detectAndSelect(
sources,
this::loadResourceFromModFile,
this::loadModFileFrom,
this::identifyMod,
this::exception
);
// 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()) {
if (dependenciesToLoad.isEmpty()) {
LOGGER.info("No dependencies to load found. Skipping!");
return Collections.emptyList();
}
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, id -> 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());
LOGGER.info("Found {} dependencies adding them to mods collection", dependenciesToLoad.size());
return dependenciesToLoad;
}
@Override
@@ -305,7 +76,89 @@ public class JarInJarDependencyLocator extends AbstractModProvider implements ID
return IModFile.Type.GAMELIBRARY.name();
}
protected static String identifyMod(IModFile modFile) {
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(JarInJarDependencyLocator::buildExceptionData)
.toList();
return new EarlyLoadingException(failedDependencies.size() + " Dependency restrictions were not met.", null, errors);
}
@NotNull
private static EarlyLoadingException.ExceptionData buildExceptionData(JarSelector.ResolutionFailureInformation<IModFile> entry) {
return new EarlyLoadingException.ExceptionData(
getErrorTranslationKey(entry),
entry.identifier().group() + ":" + entry.identifier().artifact(),
entry.sources()
.stream()
.flatMap(JarInJarDependencyLocator::getModWithVersionRangeStream)
.map(JarInJarDependencyLocator::formatError)
.collect(Collectors.joining(", "))
);
}
@NotNull
private static String getErrorTranslationKey(JarSelector.ResolutionFailureInformation<IModFile> entry) {
return entry.failureReason() == JarSelector.FailureReason.VERSION_RESOLUTION_FAILED ?
"fml.dependencyloading.conflictingdependencies" :
"fml.dependencyloading.mismatchedcontaineddependencies";
}
@NotNull
private static 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 static 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) {
if (modFile.getModFileInfo() != null && !modFile.getModInfos().isEmpty())
return modFile.getModInfos().stream().map(IModInfo::getModId).collect(Collectors.joining());
@@ -316,377 +169,5 @@ public class JarInJarDependencyLocator extends AbstractModProvider implements ID
return modFile.getFileName();
}
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);
}
}
private record ModWithVersionRange(IModInfo modInfo, VersionRange versionRange, ArtifactVersion artifactVersion) {}
}
@@ -14,7 +14,6 @@ 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;
@@ -44,10 +43,10 @@ public final class MinecraftLocator extends AbstractModProvider implements IModL
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", "The base Minecraft game, published by Mojang decompiled and deobfuscated by Forge");
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");
conf.set("mods", List.of(mods));
var configWrapper = new NightConfigWrapper(conf);
@@ -58,9 +57,4 @@ public final class MinecraftLocator extends AbstractModProvider implements IModL
public String name() {
return "minecraft";
}
@Override
public void scanFile(IModFile file, Consumer<Path> pathConsumer) {
// skip annotation scanning for MC itself
}
}
@@ -19,7 +19,7 @@ import org.objectweb.asm.Type;
import com.google.common.base.MoreObjects;
@ApiStatus.Internal
final class ModAnnotation {
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,7 +84,6 @@ final class ModAnnotation {
}
public void endArray() {
arrayList.trimToSize();
values.put(arrayName, arrayList);
arrayList = null;
}
@@ -22,47 +22,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
@ApiStatus.Internal
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;"
);
}
}
class ModClassVisitor extends ClassVisitor {
private Type asmType;
private Type asmSuperType;
private Set<Type> interfaces;
@@ -75,17 +35,12 @@ final 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() ? switch (superName) {
case "java/lang/Object" -> OBJECT_TYPE;
case "java/lang/Record" -> RECORD_TYPE;
default -> Type.getObjectType(superName);
} : null;
this.asmSuperType = superName != null && !superName.isEmpty() ? 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);
@@ -96,7 +51,6 @@ final 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);
@@ -109,7 +63,6 @@ final 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);
@@ -125,7 +78,7 @@ final class ModClassVisitor extends ClassVisitor {
}
}
private final class ModAnnotationVisitor extends AnnotationVisitor {
private class ModAnnotationVisitor extends AnnotationVisitor {
private final ModAnnotation annotation;
private boolean array;
private boolean isSubAnnotation;
@@ -5,6 +5,7 @@
package net.minecraftforge.fml.loading.moddiscovery;
import com.google.common.collect.ImmutableList;
import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.api.IModuleLayerManager;
@@ -34,36 +35,40 @@ import java.util.ServiceLoader;
import java.util.stream.Collectors;
@ApiStatus.Internal
public final class ModDiscoverer {
private ModDiscoverer() {}
public class 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;
@SuppressWarnings("removal")
public static ModValidator discoverMods(Map<String, ?> arguments) {
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();
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();
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());
for (IModLocator iModLocator : modLocatorList) {
iModLocator.initArguments(arguments);
}
List<IDependencyLocator> dependencyLocatorList = ServiceLoaderUtils.streamWithErrorHandling(dependencyLocators, sce -> LOGGER.error("Failed to load dependency locator list", sce)).toList();
dependencyLocatorList = ServiceLoaderUtils.streamServiceLoader(()-> dependencyLocators, sce->LOGGER.error("Failed to load dependency locator list", sce)).collect(Collectors.toList());
for (IDependencyLocator l : dependencyLocatorList) {
l.initArguments(arguments);
}
if (LOGGER.isDebugEnabled(LogMarkers.CORE)) {
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(",")));
.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(",")));
.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<>();
@@ -111,7 +116,7 @@ public final class ModDiscoverer {
}
LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} valid mod files", locator, locatedFiles.size());
handleLocatedFiles(loadedFiles, locatedFiles, locator);
handleLocatedFiles(loadedFiles, locatedFiles);
} 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);
@@ -125,7 +130,8 @@ public final 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 = new EnumMap<>(IModFile.Type.class);
try {
final UniqueModListBuilder.UniqueModListData uniqueModsData = UniqueModListBuilder.buildUniqueList(loadedFiles);
final UniqueModListBuilder modsUniqueListBuilder = new UniqueModListBuilder(loadedFiles);
final UniqueModListBuilder.UniqueModListData uniqueModsData = modsUniqueListBuilder.buildUniqueList();
//Grab the temporary results.
//This allows loading to continue to a base state, in case dependency loading fails.
@@ -145,10 +151,14 @@ public final class ModDiscoverer {
for (IDependencyLocator locator : dependencyLocatorList) {
try {
LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator);
final List<IModFile> locatedMods = List.copyOf(loadedFiles);
final List<IModFile> locatedMods = ImmutableList.copyOf(loadedFiles);
var locatedFiles = locator.scanMods(locatedMods);
handleLocatedFiles(loadedFiles, locatedFiles, locator);
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);
}
catch (EarlyLoadingException exception) {
LOGGER.error(LogMarkers.SCAN, "Failed to load dependencies with locator {}", locator, exception);
@@ -158,7 +168,8 @@ public final 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.UniqueModListData uniqueModsAndDependenciesData = UniqueModListBuilder.buildUniqueList(loadedFiles);
final UniqueModListBuilder modsAndDependenciesUniqueListBuilder = new UniqueModListBuilder(loadedFiles);
final UniqueModListBuilder.UniqueModListData uniqueModsAndDependenciesData = modsAndDependenciesUniqueListBuilder.buildUniqueList();
//We now only need the mod files map, not the list.
modFilesMap = uniqueModsAndDependenciesData.modFiles().stream()
@@ -181,14 +192,11 @@ public final class ModDiscoverer {
return validator;
}
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);
}
private static 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());
}
loadedFiles.addAll(locatedModFiles);
}
}
@@ -21,13 +21,13 @@ 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,15 +39,17 @@ 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 IModProvider provider;
private final IModFileInfo modFileInfo;
private IModFileInfo modFileInfo;
private ModFileScanData fileModFileScanData;
private CompletableFuture<ModFileScanData> futureScanResult;
private List<CoreModFile> coreMods;
private Path accessTransformer;
private List<Path> accessTransformers = Collections.emptyList();
@@ -61,11 +63,12 @@ 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;
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, parser);
this.modFileInfo = ModFileParser.readModList(this, this.parser);
}
@Override
@@ -92,22 +95,31 @@ public class ModFile implements IModFile {
return modFileInfo.getMods();
}
@Deprecated(forRemoval = true, since = "1.21.11")
/** @deprecated Use {@link #getAccessTransformers()} instead*/
@Deprecated(forRemoval=true, since="1.21.11")
public Optional<Path> getAccessTransformer() {
return Optional.ofNullable(accessTransformer);
}
public List<Path> getAccessTransformers() {
return this.accessTransformers;
return 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.getFileName(), this.modFileInfo.requiredLanguageLoaders());
var cfg = this.modFileInfo.getConfig()
.<List<String>>getConfigElement("accessTransformers")
.orElse(null);
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()));
List<String> cfg;
// Note: Some mods may have invalid tomls copied from other projects.
// Unfortunately we must protect against those landmines.
try {
cfg = this.modFileInfo.getConfig().<List<String>>getConfigElement("accessTransformers").orElse(null);
} catch (Exception e) {
cfg = null;
LOGGER.warn("{} contains an invalid 'accessTransformers' TOML entry. Should be e.g. accessTransformers = [\"META-INF/accesstransformer.cfg\", \"META-INF/extra_at.cfg\"] or accessTransformers = [] for no ATs. Falling back to default.", this.getFileName());
}
if (cfg == null) {
var path = findResource("META-INF", "accesstransformer.cfg");
if (Files.exists(path)) {
@@ -116,14 +128,18 @@ public class ModFile implements IModFile {
}
} else if (!cfg.isEmpty()) {
var paths = new ArrayList<Path>(cfg.size());
for (var path : cfg)
paths.add(getSecureJar().getPath(path.replace('\\', '/')));
for (var path : cfg) {
paths.add(getSecureJar().getPath(path.replace("\\","/")));
}
this.accessTransformers = List.copyOf(paths);
}
return true;
}
public List<CoreModFile> getCoreMods() {
return coreMods;
}
/**
* Run in an executor thread to harvest the class and annotation list
*/
@@ -186,33 +202,17 @@ public class ModFile implements IModFile {
var service = services.findLanguage(this, spec.languageName(), spec.acceptedVersions());
lst.add(service);
}
this.loaders = List.copyOf(lst);
this.loaders = Collections.unmodifiableList(lst);
}
@Override
public String toString() {
var path = getFilePath();
return "Mod File: " + path == null ? "null" : path.toUri().toString();
return "Mod File: " + Objects.toString(this.jar.getPrimaryPath());
}
@Override
public String getFileName() {
// 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;
return getFilePath().getFileName().toString();
}
@Override
@@ -264,7 +264,7 @@ public class ModFileInfo implements IModFileInfo, IConfigurable {
@Override
public String versionString() {
return getMods().getFirst().getVersion().toString();
return getMods().get(0).getVersion().toString();
}
@Override
@@ -7,6 +7,8 @@ package net.minecraftforge.fml.loading.moddiscovery;
import com.electronwill.nightconfig.core.file.FileConfig;
import com.electronwill.nightconfig.core.io.ParsingException;
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,8 +18,13 @@ import net.minecraftforge.forgespi.locating.ModFileLoadingException;
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,6 +53,26 @@ public class ModFileParser {
LOGGER.error("Mod candidate {} contains a corrupt or misconfigured toml.", modFile.getFileName());
throw new ModFileLoadingException("Mod candidate " + modFile.getFileName() + " contains a corrupt or misconfigured toml.");
}
}
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();
}
}
@@ -34,6 +34,7 @@ public class ModValidator {
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));
@@ -43,6 +44,7 @@ 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) {
@@ -50,9 +52,7 @@ public class ModValidator {
}
private static List<ModFile> lst(List<ModFile> files) {
if (files == null) return new ArrayList<>();
if (files instanceof ArrayList) return files;
return new ArrayList<>(files);
return files == null ? new ArrayList<>() : new ArrayList<>(files);
}
public void stage1Validation() {
@@ -60,7 +60,7 @@ 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
@@ -125,6 +125,7 @@ public class ModValidator {
allErrors.addAll(this.discoveryErrorData);
loadingModList = ModSorter.sort(candidateMods, allErrors);
loadingModList.addCoreMods();
loadingModList.addAccessTransformers();
loadingModList.setBrokenFiles(brokenFiles);
BackgroundScanHandler backgroundScanHandler = new BackgroundScanHandler(candidateMods);
@@ -17,6 +17,7 @@ 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;
@@ -81,10 +81,6 @@ class ArgumentList {
.toArray(String[]::new);
}
public boolean has(String key) {
return this.values.containsKey(key);
}
public boolean hasValue(String key) {
return getOrDefault(key, null) != null;
}
@@ -22,10 +22,6 @@ 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; }
@@ -33,35 +29,13 @@ 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 args.getArguments();
return arguments;
if (isData())
return args.getArguments();
return arguments;
var args = ArgumentList.from(arguments);
String username = args.get("username");
if (username != null) { // Replace '#' placeholders with random numbers
@@ -156,6 +130,7 @@ abstract class CommonDevLaunchHandler extends CommonLaunchHandler {
return new String[] {
"net/minecraftforge/",
"META-INF/services/",
"META-INF/coremods.json",
"META-INF/mods.toml"
};
}
@@ -29,27 +29,17 @@ import java.util.List;
public abstract class CommonLaunchHandler implements ILaunchHandlerService {
protected static final Logger LOGGER = LogUtils.getLogger();
private final String name;
protected Dist dist = Dist.CLIENT;
protected boolean isData = false;
protected String module;
protected String main;
protected final LaunchType type;
private final String prefix;
protected CommonLaunchHandler(LaunchType type, String prefix) {
this(prefix + type.name());
this.dist = type.dist();
this.isData = type.data();
this.module = type.module();
this.main = type.main();
this.type = type;
this.prefix = prefix;
}
protected CommonLaunchHandler(String name) {
this.name = name;
}
@Override public String name() { return this.name; }
public Dist getDist() { return this.dist; }
public boolean isData() { return this.isData; }
@Override public String name() { return this.prefix + this.type.name(); }
public Dist getDist() { return this.type.dist(); }
public boolean isData() { return this.type.data(); }
public boolean isProduction() { return false; }
public abstract String getNaming();
@@ -76,15 +66,14 @@ public abstract class CommonLaunchHandler implements ILaunchHandlerService {
}
protected ServiceRunner makeService(final String[] arguments, final ModuleLayer gameLayer) {
return () -> runTarget(this.module, this.main, arguments, gameLayer);
return () -> runTarget(this.type.module(), this.type.main(), arguments, gameLayer);
}
protected record LaunchType(String name, String module, String main, Dist dist, boolean data) {};
protected static final LaunchType CLIENT = new LaunchType("client", "minecraft", "net.minecraft.client.main.Main", Dist.CLIENT, false);
protected static final LaunchType CLIENT_DATA = new LaunchType("client_data", "minecraft", "net.minecraft.client.data.Main", Dist.CLIENT, true);
protected static final LaunchType DATA = new LaunchType("data", "minecraft", "net.minecraft.data.Main", Dist.CLIENT, true);
protected static final LaunchType SERVER = new LaunchType("server", "minecraft", "net.minecraft.server.Main", Dist.DEDICATED_SERVER, false);
protected static final LaunchType SERVER_GAMETEST = new LaunchType("server_gametest", "minecraft", "net.minecraft.gametest.Main", Dist.DEDICATED_SERVER, false);
protected static final LaunchType SERVER_GAMETEST = new LaunchType("server_gametest", "net.minecraftforge.forge", "net.minecraftforge.gametest.GameTestMain", Dist.DEDICATED_SERVER, false);
protected void runTarget(String module, String target, final String[] arguments, final ModuleLayer layer) throws Throwable {
var mod = layer.findModule(module).orElse(null);
@@ -14,14 +14,12 @@ sealed abstract class ForgeDevLaunchHandler extends CommonDevLaunchHandler {
super(type, "forge_dev_");
}
private ForgeDevLaunchHandler(String name) {
super(name);
}
@Override
public List<Path> getMinecraftPaths() {
// The client extra jar is on the classpath we can try locating it using the .mcassetsroot which vanilla uses for loading data
var extra = getPathFromResource("assets/.mcassetsroot");
// The extra jar is on the classpath, so try and pull it out of the legacy classpath
var legacyCP = findClassPath();
var extra = findJarOnClasspath(legacyCP, "client-extra");
// Minecraft is an exploded directory, so find it.
var minecraft = getPathFromResource("net/minecraft/client/Minecraft.class");
var forge = getPathFromResource("net/minecraftforge/common/MinecraftForge.class");
@@ -41,12 +39,6 @@ sealed abstract class ForgeDevLaunchHandler extends CommonDevLaunchHandler {
}
}
public static final class ClientData extends ForgeDevLaunchHandler {
public ClientData() {
super(CLIENT_DATA);
}
}
public static final class Data extends ForgeDevLaunchHandler {
public Data() {
super(DATA);
@@ -64,10 +56,4 @@ sealed abstract class ForgeDevLaunchHandler extends CommonDevLaunchHandler {
super(SERVER_GAMETEST);
}
}
public static final class Custom extends ForgeDevLaunchHandler {
public Custom() {
super("forge_dev");
}
}
}
@@ -96,14 +96,14 @@ public final class ForgeDevLocator extends AbstractModProvider implements IModLo
// Find resource directories for every mod in this group
var paths = new LinkedHashSet<Path>();
for (var modid : modids.stream().sorted().toList()) {
for (var modid : modids) {
var rsc = path.resolve(modid);
if (Files.exists(rsc))
paths.add(rsc);
}
var root = memory.getPath(modids.iterator().next()); // use the first modid as our root
buildModsToml(paths, modids, root, pkg.startsWith("net/minecraftforge/debug/client/"));
buildModsToml(paths, modids, root);
buildPackMeta(paths, root);
moveModuleInfo(paths, root);
@@ -187,7 +187,7 @@ public final class ForgeDevLocator extends AbstractModProvider implements IModLo
}
// Builds or update the mods.toml file for all @Mods in this package
private static void buildModsToml(Set<Path> resources, Set<String> modids, Path root, boolean clientOnly) {
private static void buildModsToml(Set<Path> resources, Set<String> modids, Path root) {
var toml = resources.stream()
.map(p -> p.resolve(MODS_TOML))
.filter(Files::exists)
@@ -205,8 +205,7 @@ public final class ForgeDevLocator extends AbstractModProvider implements IModLo
var defaults = Map.of(
"modLoader", "javafml",
"loaderVersion", "[0,)",
"license", "Doesnt fucking matter",
"clientSideOnly", clientOnly
"license", "Doesnt fucking matter"
);
for (var key : defaults.keySet()) {
@@ -8,18 +8,17 @@ package net.minecraftforge.fml.loading.targets;
import net.minecraftforge.fml.loading.FMLLoader;
import java.nio.file.Path;
import java.util.List;
import java.util.function.BiPredicate;
import org.jetbrains.annotations.ApiStatus;
import cpw.mods.jarhandling.SecureJar;
@ApiStatus.Internal
sealed abstract class ForgeUserdevLaunchHandler extends CommonDevLaunchHandler {
private ForgeUserdevLaunchHandler(LaunchType type) {
super(type, "forge_userdev_");
}
private ForgeUserdevLaunchHandler(String name) {
super(name);
}
@Override
public List<Path> getMinecraftPaths() {
var legacyCP = findClassPath();
@@ -27,7 +26,7 @@ sealed abstract class ForgeUserdevLaunchHandler extends CommonDevLaunchHandler {
// Minecraft is extra jar {resources} + forge jar {patches}
// The MC extra and forge jars are on the classpath, so try and pull them out
var extra = findJarOnClasspath(legacyCP, "client-extra"); // This should be "client-" + vers.mcAndMCPVersion() + "-extra" but FG6 qwerks
var extra = findJarOnClasspath(legacyCP, "client-extra");
var forge = findJarOnClasspath(legacyCP, "forge-" + vers.mcAndForgeVersion());
// We need to filter the forge jar to just MC code
var minecraft = CommonDevLaunchHandler.getMinecraftOnly(extra, forge);
@@ -40,12 +39,6 @@ sealed abstract class ForgeUserdevLaunchHandler extends CommonDevLaunchHandler {
}
}
public static final class ClientData extends ForgeUserdevLaunchHandler {
public ClientData() {
super(CLIENT_DATA);
}
}
public static final class Data extends ForgeUserdevLaunchHandler {
public Data() {
super(DATA);
@@ -63,10 +56,4 @@ sealed abstract class ForgeUserdevLaunchHandler extends CommonDevLaunchHandler {
super(SERVER_GAMETEST);
}
}
public static final class Custom extends ForgeUserdevLaunchHandler {
public Custom() {
super("forge_userdev");
}
}
}
@@ -4,16 +4,12 @@ net.minecraftforge.fml.loading.targets.ForgeProdLaunchHandler$Server
# Mod Developers
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$Client
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$ClientData
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$Custom
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$Data
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$Server
net.minecraftforge.fml.loading.targets.ForgeUserdevLaunchHandler$ServerGameTest
#Working on Forge itself
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$Client
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$ClientData
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$Custom
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$Data
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$Server
net.minecraftforge.fml.loading.targets.ForgeDevLaunchHandler$ServerGameTest
@@ -1,4 +1,5 @@
net.minecraftforge.fml.loading.log4j.SLF4JFixerLaunchPluginService
net.minecraftforge.fml.loading.RuntimeDistCleaner
net.minecraftforge.fml.common.asm.RuntimeEnumExtender
net.minecraftforge.fml.common.asm.ObjectHolderDefinalize
net.minecraftforge.fml.common.asm.CapabilityTokenSubclass
@@ -1,35 +0,0 @@
{
"options": [
{
"resource": "com/llamalad7/mixinextras/platform/forge/MixinExtrasConfigPlugin.class",
"layer": "GAMELIBRARY",
"id": "mixinextras",
"deps": [
{
"identifier": {
"group": "com.github.LlamaLad7",
"artifact": "MixinExtras"
},
"version": {
"range": "[0.5.4,)",
"artifactVersion": "0.5.4"
},
"path": "",
"isObfuscated": false
}
],
"meta": {
"identifier": {
"group": "io.github.llamalad7",
"artifact": "mixinextras-forge"
},
"version": {
"artifactVersion": "0.5.4"
},
"path": "",
"isObfuscated": false
},
"nested": true
}
]
}
+1 -1
View File
@@ -4,7 +4,7 @@
SPDX-License-Identifier: LGPL-2.1-only
-->
<Configuration status="warn" shutdownHook="disable">
<Configuration status="warn" packages="net.minecrell.terminalconsole,net.minecrell.terminalconsole.util,com.mojang.util,cpw.mods.modlauncher.log,net.minecraftforge.fml.loading.log4j" shutdownHook="disable">
<filters>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="${sys:forge.logging.marker.networking:-DENY}" onMismatch="NEUTRAL"/>
-78
View File
@@ -1,78 +0,0 @@
plugins {
id 'java-library'
id 'maven-publish'
alias libs.plugins.licenser
alias libs.plugins.gradleutils
alias libs.plugins.gitversion
alias libs.plugins.changelog
id 'net.minecraftforge.forge.build.convention'
}
gradleutils.displayName = 'Forge Transformers'
final vendor = 'Forge Development LLC'
description = 'Forge-specific transformers unrelated to the FML project.'
java {
toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
withSourcesJar()
}
license {
header = rootProject.file('LICENSE-header.txt')
}
changelog {
from changelogBase
}
dependencies {
compileOnly(libs.jetbrains.annotations)
implementation(libs.bundles.asm ){ transitive = false } // Needed by all the black magic
implementation(libs.log4j.api ){ transitive = false }
implementation(libs.gson ){ transitive = false }
implementation(libs.modlauncher ){ transitive = false }
implementation(libs.coremods.api){ transitive = false }
testCompileOnly(libs.jetbrains.annotations)
}
tasks.named('jar', Jar) {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.forge.transformers',
'Forge-Module-Layer' : 'boot'
])
attributes([
'Specification-Title' : gradleutils.displayName.get(),
'Specification-Vendor' : vendor,
'Specification-Version' : '1',
'Implementation-Title' : gradleutils.displayName.get(),
'Implementation-Vendor' : vendor,
'Implementation-Version': forgeVersion
], 'net/minecraftforge/forge/transformers/')
}
}
publishing {
repositories {
maven gradleutils.publishingForgeMaven
}
publications.register('mavenJava', MavenPublication) {
changelog.publish(it)
gradleutils.promote(it)
from components.java
pom { pom ->
description = project.description
gradleutils.pom.addRemoteDetails(pom)
licenses {
license gradleutils.pom.licenses.LGPLv2_1
}
}
}
}
@@ -1,158 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.transformers;
import cpw.mods.modlauncher.api.ITransformer;
import cpw.mods.modlauncher.api.ITransformerVotingContext;
import cpw.mods.modlauncher.api.TransformerVoteResult;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
record FieldToMethodTransformer(String className, Map<String, String> fields) implements ITransformer<ClassNode> {
private static final Logger LOGGER = LogManager.getLogger();
// TODO [Forge][Transformer] Make this properly data driven or configurable.
// It was hard-coded like this before when using JS CoreMods, though.
static final Map<String, Map<String, String>> TARGETS = Map.of(
"net.minecraft.world.level.biome.Biome",
Map.of(
"climateSettings", "getModifiedClimateSettings",
"specialEffects", "getModifiedSpecialEffects"
),
"net.minecraft.world.effect.MobEffectInstance",
Map.of(
"effect", "getEffect"
),
"net.minecraft.world.level.block.LiquidBlock",
Map.of(
"fluid", "getFluid"
),
"net.minecraft.world.item.BucketItem",
Map.of(
"content", "getFluid"
),
"net.minecraft.world.level.block.FlowerPotBlock",
Map.of(
"potted", "getPotted"
)
);
@SuppressWarnings("rawtypes")
static List<ITransformer> getAll() {
var ret = new ArrayList<ITransformer>(TARGETS.size());
for (var entry : TARGETS.entrySet()) {
ret.add(new FieldToMethodTransformer(entry.getKey(), entry.getValue()));
}
return ret;
}
@Override
public @NotNull ClassNode transform(ClassNode input, ITransformerVotingContext context) {
for (var entry : this.fields.entrySet()) {
redirectFieldToMethod(input, entry.getKey(), entry.getValue());
}
return input;
}
@Override
public @NotNull TransformerVoteResult castVote(ITransformerVotingContext context) {
return TransformerVoteResult.YES;
}
@Override
public @NotNull Set<Target> targets() {
return Set.of(Target.targetClass(this.className));
}
private static final @Nullable FieldNode findField(final ClassNode classNode, final String name) {
FieldNode ret = null;
for (var fieldNode : classNode.fields) {
if (name.equals(fieldNode.name)) {
if (ret != null)
throw new IllegalStateException("Found multiple fields with name " + name);
ret = fieldNode;
}
}
return ret;
}
private static final @Nullable MethodNode findMethod(final ClassNode classNode, final String name, final String desc) {
for (var methodNode : classNode.methods) {
if (name.equals(methodNode.name) && desc.equals(methodNode.desc))
return methodNode;
}
return null;
}
/**
* Rewrites accesses to a specific field in the given class to a method-call.
* <p>
* The field specified by fieldName must be private and non-static. The method-call the field-access is redirected
* to does not take any parameters and returns an object of the same type as the field. If no methodName is passed,
* any method matching the described signature will be used as callable method.
*
* @param classNode the class to rewrite the accesses in
* @param fieldName the field accesses should be redirected to
* @param methodName the name of the method to redirect accesses through
* @apiNote This method was written as a special use case for Forge. It is not recommended to use this method
* unless you know what you are doing.
*/
private static void redirectFieldToMethod(final ClassNode classNode, final String fieldName, final String methodName) {
var foundField = findField(classNode, fieldName);
if (foundField == null)
throw new IllegalStateException("No field with name " + fieldName + " found");
if (!Modifier.isPrivate(foundField.access) || Modifier.isStatic(foundField.access))
throw new IllegalStateException("Field " + fieldName + " is not private and an instance field");
var methodSignature = "()" + foundField.desc;
var foundMethod = findMethod(classNode, methodName, methodSignature);
if (foundMethod == null)
throw new IllegalStateException("Unable to find method " + methodName + methodSignature);
for (MethodNode methodNode : classNode.methods) {
// skip the found getter method
if (methodNode == foundMethod) continue;
// Why is this filtered? UI twas added in the beginning but would it matter?
if (methodSignature.equals(foundMethod)) {
LOGGER.debug("FieldToMethodTransformer " + classNode.name + " skipping " + methodNode.name + methodNode.hashCode());
continue;
}
for (var insn : methodNode.instructions) {
if (insn.getOpcode() != Opcodes.GETFIELD)
continue;
var fieldInsnNode = (FieldInsnNode)insn;
if (fieldName.equals(fieldInsnNode.name)) {
var replace = new MethodInsnNode(Opcodes.INVOKEVIRTUAL, classNode.name, foundMethod.name, foundMethod.desc, false);
methodNode.instructions.set(insn, replace);
}
}
}
}
}
@@ -1,34 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.transformers;
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.ITransformationService;
import cpw.mods.modlauncher.api.ITransformer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
public record ForgeModTransformers(String name) implements ITransformationService {
public ForgeModTransformers() {
this("forge");
}
@Override
public void initialize(IEnvironment environment) { }
@Override
public void onLoad(IEnvironment env, Set<String> otherServices) { }
@Override
@SuppressWarnings("rawtypes")
public @NotNull List<ITransformer> transformers() {
List<ITransformer> transformers = FieldToMethodTransformer.getAll();
transformers.add(new MethodRedirector());
return transformers;
}
}
@@ -1,152 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.forge.transformers;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import cpw.mods.modlauncher.api.ITransformer;
import cpw.mods.modlauncher.api.ITransformerVotingContext;
import cpw.mods.modlauncher.api.TransformerVoteResult;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
// NOTE: THIS IS A SINGLETON!
record MethodRedirector() implements ITransformer<ClassNode> {
private static final Logger LOGGER = LogManager.getLogger();
private static final Gson GSON = new Gson();
private static final Replacement[] REPLACEMENTS = {
new Replacement(
// finalizeSpawn redirection to ForgeEventFactory.onFinalizeSpawn
Opcodes.INVOKEVIRTUAL,
"finalizeSpawn",
"(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/EntitySpawnReason;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;",
GSON.fromJson(new InputStreamReader(sneak(() -> MethodRedirector.class.getModule().getResourceAsStream("coremods/finalize_spawn_targets.json"))), Target[].class),
insn -> new MethodInsnNode(
Opcodes.INVOKESTATIC,
"net/minecraftforge/event/ForgeEventFactory",
"onFinalizeSpawn",
"(Lnet/minecraft/world/entity/Mob;Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/EntitySpawnReason;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;",
false
)
)
};
private static <T> T sneak(Callable<T> callable) {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
record Replacement(
int opcode,
String name,
String desc,
Target[] targets,
UnaryOperator<MethodInsnNode> factory
) { }
record Target(
@SerializedName("class") String className,
String[] methods
) { }
@Override
public @NotNull TransformerVoteResult castVote(ITransformerVotingContext context) {
return TransformerVoteResult.YES;
}
@Override
public @NotNull Set<ITransformer.Target> targets() {
return Arrays
.stream(REPLACEMENTS)
.map(Replacement::targets)
.flatMap(Arrays::stream)
.map(Target::className)
.map(ITransformer.Target::targetClass)
.collect(Collectors.toUnmodifiableSet());
}
@Override
public @NotNull ClassNode transform(ClassNode clazz, ITransformerVotingContext context) {
for (var replacement : REPLACEMENTS) {
for (var methodString : getClassTargetMethods(clazz, replacement)) {
// cut up the string since we've put both the name and desc as the target
var splitPos = methodString.indexOf('(');
var methodName = methodString.substring(0, splitPos);
var methodDesc = methodString.substring(splitPos);
var method = findMethodNode(clazz, methodName, methodDesc);
if (method == null) {
LOGGER.error("Failed to redirect method call for {}! Method {} not found in class {}! This is a Forge bug, and is likely due to a Minecraft update changing something.", replacement.name, methodString, clazz.name);
continue;
}
for (var insn : method.instructions) {
if (!shouldReplace(insn, replacement)) continue;
var methodInsn = (MethodInsnNode) insn;
var redirection = replacement.factory.apply(methodInsn);
LOGGER.debug("Redirecting method call {}{} to {}{} inside of {}.{}", methodInsn.name, methodInsn.desc, redirection.name, redirection.desc, clazz.name, method.name);
method.instructions.set(insn, redirection);
}
}
}
return clazz;
}
private static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc) {
for (var method : clazz.methods) {
if (name.equals(method.name) && desc.equals(method.desc))
return method;
}
return null;
}
private static boolean shouldReplace(AbstractInsnNode insn, Replacement replacement) {
return insn.getOpcode() == replacement.opcode
&& replacement.name.equals(((MethodInsnNode) insn).name)
&& replacement.desc.equals(((MethodInsnNode) insn).desc);
}
private static List<String> getClassTargetMethods(ClassNode clazz, Replacement replacement) {
for (var t : replacement.targets) {
if (!t.className.equals(clazz.name)) continue;
// declared methods
var targets = new ArrayList<>(Arrays.asList(t.methods));
// synthetic methods
for (var method : clazz.methods) {
if ((method.access & Opcodes.ACC_SYNTHETIC) != 0)
targets.add(method.name + method.desc);
}
return targets;
}
return List.of();
}
}
Loaded 100 of 1865 files, more files were not shown because too many files have changed in this diff. Show more