Compare commits

...
467 Commits
Author SHA1 Message Date
LexManos 7878d97fbb Add protection against large network channel lists 2026-10-14 20:04:33 -07:00
LexManos 8b2980400b 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:05:59 -07:00
BonsUnleashedandLexManos 58c92b1e76 Store object holder handlers per registry name (#10899)
Co-authored-by: LexManos <[email protected]>
2026-10-12 17:14:32 -07:00
LexManos 01568f90fa Fix generic errors in new Eclipse compiler 2026-10-12 17:14:26 -07:00
Meme Man fba9c66164 Update equality check for RegistryObject to include RegistryObject#key. (#10864) 2026-08-04 19:10:36 +01:00
Matt afaba58edc Toml parsing errors are now visible in the latest log (#10842) 2026-07-09 13:01:35 -07:00
Jonathing 8bdf29553b Migrate the MDK to ForgeGradle 7
Backport of d29524b to 1.21.3
2026-03-18 23:10:05 +00:00
Paint_Ninja f6867c2151 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:56:04 +00:00
Paint_Ninja 63fa38ba14 Skip processing vanilla classes in RuntimeDistCleaner when running on a client
Backport of #10682 to 1.21.3
2026-03-17 20:43:17 +00:00
LexManos e6318f8793 Update SecureModules to address ZipPath bug with brackets. Closes #10755 2026-01-20 18:45:24 -08:00
LexManos 0bee88f4c0 Mark applyServerBinpatches as a dependency of BundleList. Fixes: #10729 2025-12-04 09:19:42 -08:00
LexManos 75650bde6b Migrate to Github Actions 2025-12-02 14:47:59 -08:00
MattandLexManos 29f1a5c117 [1.21.3] Fix crafter not inserting into iitemhandler (#10667) (#10670)
Co-authored-by: LexManos <[email protected]>
2025-09-25 13:36:49 -07:00
Paint_Ninja aee3baf4ce Bump ASM to 9.8 for Java 25 runtime support 2025-09-22 11:54:36 +01:00
Matt c73c954e46 Add ForgeLayeredDraw & AddGuiOverlayLayersEvent as API for gui overlay management. (#10580) 2025-07-27 10:17:44 -07:00
Jonathing 371938199e Add #forge:chorus_additionally_grows_on (#10460) 2025-04-11 09:49:46 -07:00
Jonathing c047cf136c 1.21.3 RB 1
https://forums.minecraftforge.net/topic/154393-forge-531-minecraft-1213/
2025-03-01 16:56:32 -05:00
Jonathing f4287ecc81 Add missed license headers (#10475) 2025-03-01 21:35:03 +00:00
JonathingandLexManos 364a1e737f Speed up mod annotation scanning by ~30% (#10466)
Co-authored-by: LexManos <[email protected]>
2025-03-01 21:34:40 +00:00
Jonathing 45a977bb33 [1.21.3] Bump dependencies, most notably EventBus (#10385)
- 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-12 17:51:46 -08:00
Jonathing ab00c907e7 Add fast graphics render type to block model jsons (#10395)
Make modded leaves behave like vanilla leaves by default (Fixes #10389)
2025-02-12 17:39:27 -08:00
979abb2b93 Re-introduce ItemStack Capabilities (#10362)
Co-authored-by: RealMangoRage <[email protected]>
Co-authored-by: LexManos <[email protected]>
2025-02-12 17:30:29 -08:00
Jonathing 056360b050 Re-introduce IForgeItem.damageItem when an item takes damage, Fixes #10344 (#10372) 2025-02-12 17:29:44 -08:00
Jonathing 960fce505c Add updated tag context to AddReloadListenerEvent (#10416)
Fixes using custom tags in GlobalLootModifiers.
2025-02-12 17:21:30 -08:00
JonathingandLexManos ea597ff45a Fix redstone update order not matching vanilla. (#10392)
Fix redstone updates for comparators going up/down incorrectly. Related: #9973
Fix game test structure rotations being inconsistant (#10391)

Co-authored-by: LexManos <[email protected]>
2025-02-12 17:11:56 -08:00
Jonathing f35dce7eb3 Fix corrupted and invalidly symlinked worlds crashing on level select (#10415) 2025-02-12 17:03:06 -08:00
Jonathing ef224bd2b8 Fix ModelDiscovery using Item components early (#10418) 2025-02-12 17:02:52 -08:00
Jonathing 0749f49b6c Fix forced chunks never being ticked by the server (#10365) 2025-02-12 13:37:21 -08:00
Jonathing 5c508efaa9 Fix shield disabling being completely ignored (#10317) 2025-01-27 15:28:07 -08:00
JonathingandJoseph T. McQuigg dbafe039d8 Fix issues in VillagerTrades.EmeraldsForVillagerTypeItem related to custom Villager Types (#10357)
Co-authored-by: Joseph T. McQuigg <[email protected]>
2025-01-27 15:27:42 -08:00
JonathingandLexManos b36ee22925 GameTestHelper additions (#10339)
Co-authored-by: LexManos <[email protected]>
2025-01-27 09:44:26 -08:00
Jonathing 287ee7b0b5 Update Gradle to 8.12.1 (#10353)
IntelliJ download sources in MDK
2025-01-27 09:44:11 -08:00
Jonathing 5f67621477 Fix JOpt Simple by bumping ModLaucnher (#10348) 2025-01-27 09:43:49 -08:00
Jonathing f237e7a8c3 Fix incorrect method reference in TntBlock.explode() (#10327) 2025-01-25 18:15:54 +00:00
Jonathing a54420de36 Fix JOpt Simple needing a strict version requirement declaration (#10312) 2025-01-25 18:15:36 +00:00
Jonathing 65dbdc8eb0 Bump dependencies (#10275)
- 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:17:38 +00:00
Jonathing d42979f620 Simplify Title Screen Brandings (#10288) 2025-01-02 21:34:03 -08:00
Jonathing ac56afc99c Clean and fix Forge's internal coremods, fixes finalizeSpawn hook. 2024-12-30 14:36:16 -08:00
Gabriel Harris-Rouquette 81b7bfe4ea Update jline for better darwin/arm64 support (#10281) 2024-12-30 09:46:25 -08:00
Jonathing 4cbcf64ce1 Bump CoreMods to 5.2.4 (#10267) 2024-12-23 15:36:20 -08:00
JonathingandPaint_Ninja 29b763a0c3 Simplify memory usage display on loading screen (#10237)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 12:52:39 -08:00
JonathingandPaint_Ninja 52f41a4448 Deprecate @ObjectHolder, add a couple of fast-paths (#10232)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 12:51:51 -08:00
JonathingandPaint_Ninja 597409e2f2 Skip Vanilla classes for the CapabilityTokenSubclass transformer (#10225)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 12:42:07 -08:00
JonathingandPaint_Ninja 21fbd72585 Skip Forge classes in the RuntimeEnumExtender transformer (#10220)
Mod classes are still transformed as usual

Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 12:30:09 -08:00
JonathingandPaint_Ninja 83a09f7584 Skip processing Forge classes in RuntimeDistCleaner (#10212)
Co-authored-by: Paint_Ninja <[email protected]>
2024-12-23 12:19:40 -08:00
Jonathing 0e864fbe6e Fix NightConfig FileWatcher causing dedicated server to hang when exiting (#10214) 2024-12-23 12:09:22 -08:00
Jonathing 2fd8900c01 Allow mipmap lowering to be disabled (#10256) 2024-12-23 11:57:00 -08:00
LexManos e643d0a11f Remove debug code. Closes #10182 2024-11-21 11:57:00 -08:00
LexManos 5efb8d3f12 Pass the Holder<Enchantment> to Item.canApplyAtEnchantingTable. Closes #10181 2024-11-21 09:52:32 -08:00
LexManos 70537542d1 Fix incorrect patch 2024-11-20 21:04:24 -08:00
LexManos bd4b059c2b Change vanilla registry order to last-seen. Closes #10179 2024-11-20 19:32:28 -08:00
LexManos e0d2c68c5b Call IUnbakedGeometry.bake from BlockModel Closes #10178 2024-11-18 12:30:22 -08:00
LexManos 86d4bda32e Cleanup some warnings 2024-11-18 12:27:16 -08:00
LexManos 98b90f26bc Fix not being able to swim in lava 2024-11-14 23:12:26 -08:00
LexManos 4e7c471fa3 Move mdk back to com/example/examplemod to align with what people expect. 2024-11-14 23:12:11 -08:00
LexManos ec6f9fc6e7 Move MDK example to test sourceset and fix Item/Block registration example. Closes #10139 2024-11-14 14:24:21 -08:00
LexManos 427fef1d4b Added support for using optional tags on Forge registries
Add DeferredRegister.key(name) helper function to make ResourceKey creation easier
Fix TagManager not binding tags
Fix Unbound Tag Exception when modders register tags during Register events Closes #10154
2024-11-14 14:23:00 -08:00
LexManos 83594b0b0a Fix empty tag conditional recipe test 2024-11-14 14:08:00 -08:00
LexManos b76064e57a Fixed missed call to getFriction hook in AbstractBoat 2024-11-12 09:33:05 -08:00
Martijn van den Brand b2bf70c7f1 Pass player argument when firing OnDatapackSyncEvent (#10170) 2024-11-11 20:39:55 -08:00
LexManos 384286a453 Cleanup lifecycle transitions
Fix mod ordering for multi-mod jars
Make dependency resolution issues easier to debug/diagnose
2024-11-11 03:04:23 -08:00
Jonathing d93b7db69a Bump CoreMods to 5.2 (#10156)
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
2024-11-08 12:28:39 -08:00
StavWasPlayZ bdbde4bb0f Fix PlayerRenderer#render invoking RenderLivingEvent and not RenderPlayerEvent (#10167) 2024-11-07 20:03:37 -08:00
LexManos e2a68ad903 Fix Powered Rails acting like Activator Rails due to misaligned patch 2024-11-06 23:38:09 -08:00
LexManos 46cfe50108 Properly allow normal mods.toml mods to specify the module names and use a full module-info.
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 16:55:10 -08:00
LexManos 4b423a3087 Fix LootTableLoadEvent not being fired. Closes #10144 2024-10-28 16:08:03 -07:00
LexManos 7d0b07e858 Fix items with use durations of 0 being repeatedly used when use button is held down. Fixed #10150 2024-10-28 14:54:39 -07:00
yurisuika 40220600b0 Add AT for BlockEntityType constructor. (#10149) 2024-10-27 18:05:19 -07:00
Jonathing 0c7a0e927c Add c:stripped_logs and c:stripped_woods tags (#10146) 2024-10-27 02:36:36 +00:00
Jonathing 60e8157b9a Bump CoreMods and ASM (#10145)
CoreMods 5.1.13 changelog: https://gist.github.com/Jonathing/905d142447ecef670526ae27243adbba
2024-10-27 02:34:06 +00:00
Paint_Ninja b8f5e4553b Remove redundant rendering options (#10140) 2024-10-26 20:24:18 +01:00
Ven de587eb50a Fix pause menu having a black background, reverts PR #10115 2024-10-26 12:22:19 -07:00
LexManos 086bcb7148 1.21.3 update 2024-10-25 18:04:34 -07:00
Joseph T. McQuigg c22589b5b3 Add c:foods/pie tag (#10135)
Replaces `forge:foods/pie`, which is now deprecated.
2024-10-17 18:40:33 +01:00
Paint_Ninja e0fbab3c49 Fix back-compat with legacy forge: tags (#10126) 2024-10-14 20:23:09 +01:00
Paint_Ninja 7cfed38194 Implement the de-facto common tags, add many new Forge tags (#9955)
https://forums.minecraftforge.net/topic/152815-common-tags-in-forge/
2024-10-13 14:38:37 +01:00
Joseph T. McQuigg b8105ca5a6 Patch BlockEntity.java to use getType Method (#10124) 2024-10-10 09:56:17 -07:00
LexManos c3ee633897 Fix and cleanup RuntimeDistCleaner 2024-10-08 14:31:17 -07:00
LexManos 23b41ce9b3 Sync component registry ids
Fix deprecation typo
Bump ModLauncher (better stack traces)
Set Automatic-Module-Name
2024-10-07 17:26:13 -07:00
StavWasPlayZ 2c6156be25 Fix layering of translucent sprites being broken by re-adding blur call Mojang removed (#10115) 2024-09-17 08:13:30 -07:00
Tygyh eeb3eb3689 Cleanup length and size checks (#10108) 2024-09-12 14:14:19 +01:00
Tygyh 44a158a714 Update vulnerable eclipse dependency (#10109) 2024-09-12 14:14:01 +01:00
LexManos 770298b1d5 Add and fire SystemMessageReceivedEvent
Replaces ClientChatReceivedEvent.System which was not fired due to Mojang changing ChatType to a registry.
2024-09-11 11:10:41 -07:00
LexManos 7bd50903a6 Fix highlighted item tooltip rendering twice. Closes #10112 2024-09-11 10:18:49 -07:00
Michael Bunting 64295d04ff Fix Network hooks not being initialized on GameTestServer (#10081)
Add rudimentary custom networking tests.
2024-09-11 09:57:17 -07:00
Michael Bunting f168ce4499 Fix inverted logic in TagEmptyCondition (#10069) 2024-09-04 12:19:15 -07:00
RealMangoRage 419c349a1f [1.21.1] Optionally supply FMLJavaModLoadingContext as a param to mod constructors (#10074)
Also made `FMLJavaModLoadingContext` extend `ModLoadingContext`. See the PR description for example usage.
2024-08-25 21:38:31 +01:00
Martijn van den Brand 26d0f7282f Fix issues with QuadBakingVertexConsumer and Forge's OBJ loader (#10065) 2024-08-22 17:26:44 -07:00
Martijn van den Brand 93c72d2d92 Fire OnDatapackSyncEvent when a player joins a server (#10076) 2024-08-22 14:37:39 -07:00
RealMangoRage f252871322 Add a few Method Helpers onto ResourceKey to make getting Data Driven Entries Simpler (#10071) 2024-08-17 20:50:07 -07:00
LexManos 32c8597305 Fix network channels not initializing during server transfers. Closes #10067 2024-08-17 13:04:02 -07:00
Paint_Ninja 9714512c31 [1.21.1] ModLoading cleanup and optimisations (#10052) 2024-08-16 22:49:11 +01:00
Yeregorix f106ff0c21 Properly ignore unknown mod files (#10063) 2024-08-14 02:17:09 -07:00
LexManos 9c85a6bb64 Add level access to Item.TooltipContext if available. 2024-08-09 19:39:00 -07:00
LexManos dc594f196a Load mods that declare explicit 1.21 compatibility as 1.21.1 is a minor bugfix. 2024-08-08 14:01:31 -07:00
LexManos db4d8a4729 1.21.1 2024-08-08 13:40:50 -07:00
andan42 0ca1d0ebd0 Removed unneeded boat patch related to MC-119811 (#10061) 2024-08-07 21:49:40 -07:00
LexManos 4995902bc0 Fix network sync of custom datapack registries.
Add gametest to verify that it functions.
2024-08-05 13:30:14 -07:00
James Mitchell 28e55b2843 Add a way to render tooltips from Formatted text and TooltipComponents elements (#10056) 2024-08-04 19:25:23 +01:00
LexManos decaace84a Fix custom creative tabs with search listing all items instead of just their own. 2024-08-01 17:21:35 -07:00
Joseph T. McQuigg 72f98dc4a3 Patch CropBlock to use instanceOf FarmBlock check instead of hard coding to Blocks.FARMLAND (#10044)
Signed-off-by: Joseph T. McQuigg <[email protected]>
2024-07-26 13:06:07 -07:00
LexManos 2951c1dac8 Fix ItemStack sensitive getDefaultAttributeModifiers not being called. 2024-07-25 16:01:53 -07:00
LexManos 40a0a16db0 Add missed @user_jvm_args.txt to run.bat 2024-07-25 16:00:04 -07:00
Martijn van den Brand 520680b037 Fix onPlaceItemIntoWorld always resetting item stack when in creative mode (#10047) 2024-07-18 12:43:11 -07:00
Martijn van den Brand 2844bead6e Fix matrix stack translations for RenderHighlightEvent (#10050) 2024-07-17 20:17:33 -07:00
LexManos d498ec4a45 Fix Global Loot Modifier deserialization not having registry access. Closes #10042 and #10043 2024-07-10 11:46:23 -07:00
LexManos e2d1ca7d0e Bump Mixin to 0.8.7 and allocate more ram to java compile tasks. 2024-07-08 13:11:00 -07:00
Paint_Ninja a547327c61 Use the new fire() and Result#isAllowed/isDenied/isDefault methods from EventBus (#10028) 2024-07-06 11:35:37 -07:00
LexManos e9a7ce9544 Some build cleanup 2024-07-02 10:25:49 -07:00
LexManos 511be6415c Make HangingSignBlockEntity useable with custom BlockEntityTypes. #10038 2024-07-02 10:25:35 -07:00
LexManos 4d85053a4d Send last PoseStack pos to RenderLevelStageEvent AFTER_PARTICLES stage. Fixes #10031 2024-07-02 10:12:16 -07:00
Jonathing 4fcf1877c1 Fix compile errors in MDK (#10029) 2024-06-24 11:52:00 -07:00
LexManos 41fd15fc57 Fix converting dirt to mud consumes whole bottle (not just emptying) Closes #10027 2024-06-21 13:34:37 -07:00
LexManos 8e0845d032 Add support for named sub-caps 2024-06-19 15:30:46 -07:00
LexManos c04e533a61 Fix conditional checking swallowing decoding errors when loading registry entries from datapacks. Fixes #9995 2024-06-19 15:09:37 -07:00
LexManos 54f4997f71 Apply license headers 2024-06-19 12:54:22 -07:00
LexManos 3272e995ab Some code cleanup in ModInfo/ModFileInfo to use less streams and make things easier to debug. 2024-06-18 11:47:11 -07:00
LexManos 8edb551571 Bump NightConfig to 3.7.3, to fix toml sub-configs being completely broken. Fixes #10015 2024-06-18 11:46:34 -07:00
LexManos 4bf0de4763 Make ForgeSpawnEgg assume color is opaque if alpha is set to 0.
Fixes "invisible" spawn eggs by default, but still allows modders to specify partial transparency.
2024-06-18 11:45:13 -07:00
vicmatskiv f6a54436b9 Fixed perspective rendering in SeparateTransformsModel (#10014) 2024-06-17 15:55:22 -07:00
Gabriel Harris-Rouquette 7a497cdbd6 Fix missed PartEntity patch in Player.attack (#10011) 2024-06-16 19:05:01 -07:00
thedarkcolour 06336a40d8 Choose default JarJar mod file type based on parent JAR Closes #9939 2024-06-14 09:50:05 -07:00
Ven 842c95cfb9 Fixed falling block entities not rendering as moving blocks (#10006) 2024-06-14 09:03:14 -07:00
LexManos aa2f471e87 Bump Night Config, Should fix #9122 2024-06-14 08:37:07 -07:00
LexManos 4209d1fc12 Fix ClientTickEvent not firing properly. 2024-06-13 17:13:15 -07:00
LexManos 583f363b6b Change tick event to have pre/post classes (#9890) 2024-06-13 15:15:04 -07:00
LexManos 1c633b4624 Fix Bogged and Wolf armor not being shearable by custom shears. Closes #10005 2024-06-13 14:21:30 -07:00
LexManos 2677511da6 Set team city branch 2024-06-13 14:20:03 -07:00
LexManos 387441c045 Remove Non-HolderLookup methods from INBTSerializeable and mark it as deprecated in favor of using Vanilla's new Component system. Closes #9998 2024-06-13 13:17:03 -07:00
4a2db246e0 1.21 Update
Co-authored-by: RealMangoRage <[email protected]>
Co-authored-by: Daniel Norris <[email protected]>
Co-authored-by: Ven <[email protected]>
2024-06-13 10:42:32 -07:00
Paint_Ninja 3d9e12a71d Cleanup FML Bindings (#10004) 2024-06-05 09:57:42 -07:00
LexManos cdeb786877 Fix NPE when calling ForgeHooks.getDefaultCreatorModId(ItemStack) on Spawn Eggs. Closes #10002 2024-06-04 15:48:07 -07:00
LexManos ff7085b95b Fix boat travel distance being incorrect. Closes #9997 2024-06-03 17:31:51 -07:00
LexManos 16dde4d551 1.20.6 RB 1
https://forums.minecraftforge.net/topic/149458-forge-501-minecraft-1206/
2024-06-03 13:15:55 -07:00
Paint_Ninja 60661350c6 Fix minor typo in MDK build.gradle 2024-06-03 20:53:10 +01:00
LexManos 7ee4a44c75 Fire EntityInteractSpecific on server side. Closes #9984 2024-06-03 12:33:53 -07:00
LexManos f22894cf70 Unlock wrapped registries when firing register events. 2024-06-03 12:31:59 -07:00
Paint_Ninja a3fce36942 Update Bootstrap to improve the Java version check error message (#9990) 2024-05-30 23:51:44 +01:00
Paint_Ninja 549dbe7df4 Minor cleanup to ModListScreen and VersionChecker (#9988) 2024-05-30 23:51:25 +01:00
Daniel Norris 5505e472aa Prevent the @OnlyIn being misused on @EventBusSubscriber and @Mod annotated classes (#9891) 2024-05-30 23:51:09 +01:00
Mysticdrew c2c7f9b656 Fix CustomizeGuiOverlayEvent.DebugText and CustomizeGuiOverlayEvent.Chat not being fired. (#9982) 2024-05-24 08:33:52 -07:00
LexManos fd8a0f9c8b Actually fix creative tab count, IDE didn't save the file before regenning patches. 2024-05-23 09:08:51 -07:00
LexManos 0ac873e296 Move Creative Inventory page count to fix issue with partially transparent tooltips. Closes #9983 2024-05-22 12:14:38 -07:00
LexManos 38b5b678cb Make OpenContainer and SpawnEntity packets process on main game thread. 2024-05-21 11:02:44 -07:00
LexManos 9a2bc1e7c2 Add File.exists check to ConfigFileTypeHandler. Closes #9976 2024-05-21 11:02:43 -07:00
RealMangoRage 95b49bedbc Fix crash when reloading a world that uses custom placed features. Closes #9979 2024-05-20 23:23:45 -07:00
LexManos 7c36115e3d Fix NPE in HurtByTargetGoal when mods set targets to null. Closes #7853 2024-05-20 16:21:17 -07:00
R0b0Li0n c037e6aca1 Fix RenderHandEvent firing with incorrect hand and item for offhand items. (#9977) 2024-05-20 11:50:09 -07:00
Mysticdrew e8f1b7ed6d Fix screen layering and re-add the test. (#9978) 2024-05-20 11:49:41 -07:00
RealMangoRage 5da16e6719 Add GatherComponentsEvent (#9944) 2024-05-14 23:02:58 -07:00
LexManos f0a82d79a6 Fix powered rails not propogating correctly. 2024-05-14 22:43:27 -07:00
LexManos 2b3a4137e1 Remove ICustomPacket and add PayloadChannel. (#9972)
New PayloadChannel that uses the vanilla payload Type for packet distinction
Implement the minecraft:register/unregister channels using the new PayloadChannel
New generic channel builder function allowing people to implement channels however they want.
2024-05-14 08:36:42 -07:00
LexManos 640c206117 Implement entity aware armor model and texture hooks. Closes #9960 2024-05-12 17:07:45 -07:00
LexManos 59a7bbdb57 Fix finalizeSpawn's return value not being used correctly. Closes #9964 2024-05-12 13:48:20 -07:00
LexManos be7ad14b4b Fix Biome patch change that caused our field redirect coremod to not function correctly. 2024-05-12 11:21:49 -07:00
LexManos c700b4ea9d Make RegistryObject.getHolder lazy, Should help cases where vanilla registries use holders from other vanilla registries. Closes #9961 2024-05-12 10:53:10 -07:00
LexManos dffa3dd4c9 Ignore jar files in the mods folder that are not Forge mods. Closes #9968 2024-05-12 03:36:22 -07:00
LexManos 4b934809c4 Fix canceling MobSpawnEvent.FinalizeSpawn causing a NPE closes #9971 2024-05-11 23:05:19 -07:00
LexManos ddf76db9b3 Fix potion brewing having arguments reversed. Closes #9970 2024-05-10 14:55:38 -07:00
LexManos 64d6b4f76a Fix Melons/Pumpkins not growing correctly. 2024-05-10 10:39:13 -07:00
Paint_Ninja 58012bfb17 Some patch cleanup (#9951) 2024-05-09 18:07:41 +01:00
LexManos 0cad6b14d9 Filter paths discovered by ServiceProvider in ClasspathLocator. Closes #9899 2024-05-09 09:39:25 -07:00
8848294210 Update SimpleChannel to make StreamCodecs easier (#9959)
Rework networking so that RegistryFriendlyByteBuf is useable for modders
Simple support for StreamCodec in SimpleChannel
Codecify all Forge packets
Make simpler builder pattern for SimpleChannel. Will eventually deprecate the old MessageBuilder as it's verbose and poorly written.

Co-authored-by: MrCrayfish <[email protected]>
Co-authored-by: Paint_Ninja <[email protected]>
2024-05-07 22:53:14 -07:00
LexManos 0e28336fc0 Fix shields not working correctly. Fixes #9966 2024-05-06 18:56:20 -07:00
LexManos 968fa9eeb5 Fix RenderTarget stencil patch location. Fixes #9965 2024-05-06 09:33:27 -07:00
EnZaXD 43768ea328 Add ModelLayers patch back (#9962) 2024-05-05 09:15:05 -07:00
LexManos 0e829630da Fix canApplyAtEnchantingTable null pointer, Closes #9956
Bump SecureModules for package info and multi-release jar fixes.
2024-05-03 14:10:17 -07:00
Paint_Ninja 0467d71466 Remove zombie chance config options (#9950) 2024-05-01 22:27:28 +01:00
Paint_Ninja 115eb5e4c8 Remove deprecated compressLanIPv6Addresses config option (#9949)
LAN IPv6 addresses are always compressed these days, so this config option is redundant
2024-05-01 22:27:16 +01:00
LexManos a7345372fd Fix custom payloads not being handled on the server in the game state. Closes #9948
Fix villagers not opening trade guis. Closes #9946
2024-05-01 13:16:41 -07:00
LexManos bd714b2f76 Fix MDK by bumping FG and disabling reobf tasks
Fix LAN server IPs being duplicated
Fix connecting to vanilla servers due to misapplied patch.
2024-05-01 11:28:43 -07:00
7b782e5b05 1.20.6 Initial Update
New Decompiler
Runtime Official mappings
ItemStacks arnt Capability providers anymore, use Vanilla's system.

Co-authored-by: RealMangoRage <[email protected]>
Co-authored-by: Paint_Ninja <[email protected]>
2024-04-30 11:42:52 -07:00
Paint_Ninja e1fa61080a Make non-Forge mods.toml detection more robust (#9935) 2024-04-25 22:36:56 +01:00
Paint_Ninja 381998c35c Fix early window crash when parsing some forms of options.txt (#9933) 2024-04-19 21:39:23 +01:00
Paint_Ninja 11ed2500a6 Fix edge-case regression with single-jar multiloader mods (#9931) 2024-04-19 21:38:57 +01:00
J-RAP a647237db4 Add ClientPauseChangeEvent (#9905) 2024-04-18 21:36:00 +01:00
Paint_Ninja e0dccae041 Improve mod loading errors (#9870) 2024-04-18 20:02:41 +01:00
Paint_Ninja e7a5c58907 Early display fixes/workarounds for buggy drivers (#9921) 2024-04-18 20:02:18 +01:00
oldkingOK e094eee54d Prevent registering null tiers (#9895)
Makes it easier to identify broken mods, as it moves the crash to when the broken mod in question registers the tier, rather than when any mod tries getting the tier.
2024-04-15 14:49:03 +01:00
Paint_NinjaandblockingHD a6adc58655 Add helper method to OnDatapackSyncEvent (#9901)
Co-authored-by: blockingHD <[email protected]>
2024-04-15 14:47:45 +01:00
J-RAP d31ea640f9 Fix NPE when acceptableValues in defineInList() does not allow nulls, fixes #9300 (#9903) 2024-04-13 12:47:35 +01:00
J-RAP 8e16765438 [1.20.x] Add ByteValue, ShortValue and FloatValue to ForgeConfigSpec, cleanup code (#9902) 2024-04-13 12:42:31 +01:00
Joseph T. McQuigg 73b0088cb4 Add Leaves method to ModelProvider.java (#9887) 2024-04-04 13:53:55 +01:00
LexManos 203fbbb742 Bump bootstrap 2024-03-18 18:31:29 -07:00
LexManos 46d78c8210 Fix level data not loading from existing worlds. Whole system needs a re-write. 2024-03-18 18:15:39 -07:00
LexManos cfba4381b6 Fix modlist size 2024-03-18 18:09:37 -07:00
LexManos 7113701322 Bump Bootstrap 2024-03-18 17:45:58 -07:00
Paint_Ninja 73e02c6add Optimize Entity capabilities a bit by reordering conditions (#9886) 2024-03-16 17:16:11 -07:00
Paint_Ninja 7f2b85d2ff [1.20.x] Make common config screen registration tasks easier (#9884) 2024-03-10 21:27:57 +00:00
Paint_Ninja 099d121c2c [1.20.4] Add CPU usage config option to early window, hide it by default (#9866) 2024-03-10 21:10:06 +00:00
Luis a6978eed68 Fix DatapackBuiltinEntriesProvider issues with forge registries, Fixes #9874 2024-03-01 13:26:00 -08:00
Luis 309116520b Readded DatapackBuiltinEntriesProvider (#9848) 2024-02-15 14:04:51 -08:00
Paint_NinjaandDennis C 3e358acbe1 Fix slightly offset mods screen link positioning (#9860)
Co-authored-by: Dennis C <[email protected]>
2024-02-15 14:04:05 -08:00
LexManos c243adf6aa Fix DNS SRV record lookup not working by hacking the module system. Closes #9846 2024-02-12 13:46:29 -08:00
LexManos 5d0047753d Add null check to DimensionDataStorage. Fixes #9859 2024-02-02 12:50:09 -08:00
LexManos aa6f268c74 Fix cases where LivingConversionEvents were not fired for vanilla conversions. Closes #9850 2024-01-29 13:13:28 -08:00
LexManos d21556a4da Update licenser plugin, and re-run it. Closes #9855 2024-01-29 12:27:45 -08:00
LexManos d4c998e311 Fix background music looping when it shouldn't 2024-01-29 12:16:46 -08:00
LexManos 25727a0e93 Fix Criterion Test Mod 2024-01-29 12:16:21 -08:00
Paint_Ninja 80286594c5 Improve help text on server Java check failure 2024-01-20 00:28:02 +00:00
Paint_Ninja 870e3ebcab [1.20.4] Restore the option of server run scripts (#9849)
Executable server jar is still a thing, but the run scripts are restored as an option for those that prefer it.
2024-01-19 20:55:00 +00:00
Paint_Ninja a4c26c9b2b [1.20.x] More buildscript clean-up (#9845) 2024-01-19 20:54:15 +00:00
Mostafa Mabrok 45b00b0004 Fixed Spelling error in credits.txt (#9694) 2024-01-09 11:42:01 -08:00
LexManos 4b1c5ddb2c Bump JarJar to fix more issues with UnionFileSystem assumptions. 2024-01-09 11:41:20 -08:00
DBotThePony 0966109602 Criterion test mod + unit test (#9744) 2024-01-09 11:40:02 -08:00
LexManos b31006534a Fix launcher version name missing - between forge and the version. Closes #9843 2024-01-08 13:43:46 -08:00
LexManos 740a5d5039 Bump JarJar and SecureModule to fix issue with jars containing [] in their name. Closes #9842 2024-01-08 13:42:36 -08:00
LexManos 817e20821d Only add sorted/deduplicated mods to the classpath.
Fixes some mods causing the Forge error displays to break. Closes #9833
2024-01-02 12:51:57 -08:00
Paint_Ninja fdb22d02cc Improve server panel compatibility (#9836) 2023-12-30 20:56:18 +00:00
Paint_Ninja f09fd1f05b Make common DisplayTest registration tasks easier (#9822) 2023-12-27 08:57:02 -08:00
LexManos ed2ba0f52c Cleanup Explosion patch but keep bin compatibility by using asm hacks. Closes #9817 2023-12-21 11:22:31 -08:00
LexManos b04118982f Bump SecureModules, Closes #9820 2023-12-18 14:24:59 -08:00
LexManos d44d0b8edc Support pack overlay system. Closes #9818 2023-12-13 14:42:49 -08:00
LexManos 6d7fda6a9b Fix Server bundle 2023-12-11 21:27:32 -08:00
Paint_Ninja 0303ca651e Optimise ForgeConfigSpec and make Range public (#9810) 2023-12-11 13:10:53 -08:00
Paint_Ninja 8445f73b2d Add clientSideOnly feature to mods.toml (#9804) 2023-12-11 13:10:33 -08:00
Paint_Ninja c7446a5180 Add impl. of IModFileInfo#showAsDataPack (#9802) 2023-12-11 13:08:50 -08:00
LexManos 5c15aa3322 Get rid of LibraryFinder 2023-12-11 12:27:23 -08:00
LexManos 588466f5e4 1.20.4 - Minor bug fix from Mojang 2023-12-07 11:35:53 -08:00
LexManos 470f03b2f9 Fix java version check in bootstrap shim 2023-12-06 16:29:53 -08:00
LexManos d2367d81fc 1.20.3 Initial Update
https://forums.minecraftforge.net/topic/139822-forge-490-minecraft-1203/
2023-12-06 10:38:20 -08:00
LexManos 6f9f0f0cfa Revive executable jars for the dedicated server
This has a minor change to development time build scripts. So developers, check the MDK
2023-12-06 10:33:58 -08:00
LexManos 16defac82e 1.20.2 RB 1 - https://forums.minecraftforge.net/topic/139824-forge-481-minecraft-1202/ 2023-12-06 10:21:59 -08:00
J-RAP 16483d4ea1 Implemented ClientPauseEvent/Hook (#9782) 2023-11-30 17:55:34 -08:00
J.T. McQuigg 28909812bb Datagen addOptionalTag/s methods that allow passing the TagKey itself instead of passing the location (#9807) 2023-11-29 19:27:54 -08:00
LexManos e5580b84b7 Deprecate EntityRenderersEvent.AddLayers functions that hard case to LivingEntityRenderers as the backing maps do not guarantee that type. Closes #9683 2023-11-28 20:55:20 -08:00
LexManos 607ca5beea Fix TagLoader error not printing tag name correctly. Closes #9693 2023-11-28 19:31:20 -08:00
LexManos 744906e33a Fix LoadingErrorScreen inner headers are not centered. Closes #9687 2023-11-28 19:26:56 -08:00
LexManos c1557ec3c2 Rework KeyModifiers system to properly allow keybinds to be triggered when multiple modifiers are pressed.
Fix setting keybinds whel using keyboard inputs to select the menu. Closes #9793
2023-11-28 19:10:07 -08:00
LexManos 4ac1a3dedf Fix KeyModifiers not being properly taken into account. Closes #9806 2023-11-28 16:16:00 -08:00
Paint_Ninjaandembeddedt a26f952212 Don't turn off VSync when rendering from Minecraft context (#9800)
Co-authored-by: embeddedt <[email protected]>
2023-11-26 16:57:01 -08:00
Paint_Ninjaandembeddedt 83ee68c0b0 [1.20.x] Fix rare crash with early display window, fixes #9673 (#9798)
Co-Authored-By: embeddedt <[email protected]>
2023-11-26 16:23:46 -08:00
DBotThePony a5ff932125 Move Chunk Watch/Unwatch events to fire after sync packets have been sent to the client (#9790)
This makes sure that modders who send extra chunk data will have clients who know about the chunk.
2023-11-17 08:45:55 -08:00
LexManos 0d820eddd4 Add more sanitization to the ChannelListManager because other projects like to impose arbitrary critical restrictions. Closes #9789 #9772 2023-11-15 09:09:18 -08:00
LexManos 83c651ce39 Fix tag loading being unordered. Closes #9774 2023-11-13 08:50:16 -08:00
Lupicus 7cd88944d4 Fix texture UV values for obj models. Closes #9737 (#9767) 2023-11-10 15:56:38 -08:00
Paint_Ninja dff52ecd71 Gradle 8.4 (#9778) 2023-11-10 11:51:08 -08:00
DBotThePony da34d97d9d Make Ingredient check for invalidation on getItems(), invalidate ingredients on tags update (#9688) 2023-11-07 21:53:27 -08:00
Paint_Ninja a27405b8d1 Improve mod description formatting in mods screen (#9768) 2023-11-02 10:53:17 -07:00
LexManos 7a443276a5 Fix lowcode implementation version, now that it's actually labeling the correct package. 2023-10-24 23:13:26 -07:00
LexManos e34090c809 Buildscript cleanup and organization.
Updated to use newer libraries that reduce the command line arguments needed to run the game.
Fix issues in the SecureModule's classloader that caused resources to not be seen when they are on parent module layers.
2023-10-24 16:24:13 -07:00
LexManos 72e8ae252c Fix @GameTestGenerator not functioning correctly. Closes #9748 2023-10-23 11:37:51 -07:00
Paint_Ninja 347c480bdc Optimise registries by using fastutils collections to minimize boxing. (#9749) 2023-10-14 10:14:03 -07:00
Paint_Ninja e0b9474621 Minor MDK changes (#9750) 2023-10-14 09:38:49 -07:00
Paint_Ninja 8ad1e899a3 Improve mod loading error message for errors inside mod constructors (#9751) 2023-10-14 09:38:29 -07:00
The_Dead_2 3b6bb5edbc Add a CrashReportAnalyser that tries to identify the mod that crashed the game (#9732) 2023-10-14 09:38:14 -07:00
LexManos c4e7d694c1 Fix potential threading issue when registering CriteriaTriggers. Closes #9745 2023-10-12 22:31:35 -07:00
LexManos e00de1a326 Move to a cache based crowdin solution instead of hitting the api every build. 2023-10-12 22:30:48 -07:00
LexManos e9ee999558 Fix Melon/Pumpkin stems having wrong plat type causing them to remain after trampling farmland. Fixes #9495 2023-10-11 22:54:55 -07:00
LexManos a1aacf25fe Fix loot tables not having their ids set. 2023-10-10 23:41:21 -07:00
LexManos 0c2f3e5523 Make horses and llama's fire LivingFallEvent. Fixes #9743 2023-10-09 11:33:39 -07:00
LexManos a6978cffbf Fix advancement screen being too dark. Fixes #9727 2023-10-09 11:14:52 -07:00
LexManos 2332ecf984 Fix ConditionalCodec having an inverted test. Closes #9742
Remove clean project on CI.
Bump FG version to fix transitive issue on module path.
2023-10-08 13:13:17 -07:00
LexManos 9cd73c84a6 More buildscript work
Fixed ignoreList for client and server.
Lazy configured a few more tasks
Fixed early loading screen hardcoded names.. why?
2023-10-07 12:41:16 -07:00
LexManos ab544a78d3 Bump guava to module path for jimfs. Fixes dedicated server. 2023-10-07 01:01:12 -07:00
LexManos 5b0abf97c8 Revive custom ingredient types. 2023-10-06 21:14:04 -07:00
LexManos 587b684035 Revive ConditionalRecipe and ConditionalAdvancement.
Also directly add conditional support to all vanilla recipe data generators.
2023-10-06 21:01:03 -07:00
LexManos 90dafb0df0 Rewrite our GameTest hooks to be sane.
Add game test to GlobalLootModifierTest which validates the smelting modifiers.
Removed FakePlayer system, it's broken and needs a major rewrite.
2023-10-06 21:01:02 -07:00
LexManos aff05a4cf3 Cleanup FMLLoader and Launch Handlers.
Cleanup some of the build.gradle
Start work on restructuring test mods. They are now treated as their own jars, as well as having their mods.toml auto-generated. No more mods.toml conflicts!
Add pattern matching support to Data run modlist.
2023-10-06 21:01:01 -07:00
DavidQF555 1ddac54256 Fix Slot Index for Inventory Ticking Items (#9736) 2023-09-29 00:14:56 -07:00
LexManos fcf9088aa5 Make default Entity.getAddPacket automatically use Forge's enhanced spawn packet if nessasary.
This spawn packet needs to be re-designed, it is effectively only there to hook into creating the new entity instance. This will be re-evaluated during the cleanup in 1.21+
2023-09-28 16:31:18 -07:00
Paint_Ninjaandembeddedt 374dbc7a4b Optimise path filter in mod loading (#9710)
Co-authored-by: embeddedt <[email protected]>
2023-09-28 14:26:43 -07:00
Jonathan Colmenares d25ba7adc8 Fix ignitedByLava making blocks permanently flammable (#9735) Fixes #9730 2023-09-27 23:11:48 -07:00
LexManos 05c88ae33a Some BackgroundScanHandler code cleanup. Closes #9713 2023-09-27 23:08:50 -07:00
LexManos e2d564d32a Fix missed patch causing new onInventoryTick method to not be called. 2023-09-27 16:38:29 -07:00
Lubyanoy Ivan 225092130e Fixed PlayerDestroyItemEvent not triggering for Fishing Rods (#9729) 2023-09-26 10:45:34 -07:00
RealMangoRage eefca22aa3 Fix migration error causing nether and end portals to not function properly. 2023-09-24 09:57:15 -07:00
Paint_Ninja 19bbb88e52 Optimise ForgeRegistry#validateContent by removing optimization that ModLauncher broke. (#9715) 2023-09-24 09:56:35 -07:00
Martijn van den Brand 18886e5128 Fix double message encoding in Channel#toVanillaPacket #9721 (#9722)
Fix OpenContainer message not resetting reader index of additional data.
2023-09-23 12:12:46 -07:00
LexManos 47dfa83cff Make CriteriaTriggers#register public again. Closes #9723 2023-09-23 12:04:50 -07:00
LexManos 0940c532f2 Fix Hunger bar rendering incorrectly. Fixes #9725 2023-09-23 12:04:49 -07:00
LexManos d80f106140 Send known channels to the server when they send us theirs. 2023-09-21 16:25:51 -07:00
LexManos 516b35ce57 1.20.2 Release 2023-09-21 11:28:37 -07:00
LexManos be60ddbfca Enable ForgeLootTableProvider. 2023-09-20 17:46:22 -07:00
LexManos af6e848227 Global Loot Modifiers 2023-09-20 15:24:46 -07:00
LexManos dd139115ed Move old tests. 2023-09-20 15:24:15 -07:00
LexManos 145df60bec 1.20.2-rc2 2023-09-18 10:53:24 -07:00
LexManos f7cca8fb88 1.20.2-rc1 2023-09-15 12:51:23 -07:00
LexManos 1f59b3f9fe 1.20.2-pre4 2023-09-13 15:15:42 -07:00
LexManos 084effdadf 1.20.2-pre3 2023-09-12 12:34:59 -07:00
LexManos 40a7998281 Use official mappings for in-repo patches, will be converted to SRG during build. 2023-09-11 14:47:34 -07:00
LexManos 4786fa6f89 1.20.2-pre2 2023-09-11 12:47:19 -07:00
LexManos 9291b865c7 Fix debug overlay text not rendering. 2023-09-11 11:36:45 -07:00
LexManos 52eca96fdc Make SimpleChannel use a varInt for the discriminator allowing essentially unlimited packets. 2023-09-10 20:56:04 -07:00
LexManos 4649f7af36 Rewrite networking. 2023-09-10 20:27:15 -07:00
LexManos 3edefbe33e 1.20.2-pre1 2023-09-08 10:56:10 -07:00
Paint_Ninjaanddhyces 12d24df4f6 Keep order of sources in PackRepository (#9702)
Co-authored-by: dhyces <[email protected]>
2023-09-02 16:21:06 -07:00
Paint_Ninja e69f5e262a Fix DelegatingPackResources searching resource path twice (#9697) 2023-08-30 14:10:17 -07:00
Paint_Ninjaandmalte0811 23cee57d88 Fix Level leak in debug HUD (#9699)
Co-authored-by: malte0811 <[email protected]>
2023-08-30 14:09:51 -07:00
Paint_Ninja ab70bde1d6 Fix PlayerSpawnPhantomsEvent not being fired (#9689) 2023-08-11 10:42:31 -07:00
Meldexun f65381fbff Enhance LivingBreathEvent and LivingDrownEvent. Closes #9680
Also remove 3.5MB of useless data from the installer.
2023-07-24 14:42:24 -07:00
LexManos b680d09359 Partially revert LazyOptional changes, now no longer internally uses weak references. 2023-07-22 08:57:25 -07:00
LexManos 7969daf81a Make LazyOptional's internal references to invalidation listeners use WeakReference, and allow modders to unregister themselves. Closes #8805 2023-07-21 23:46:26 -07:00
LexManos f157567d2a Revert EntityEvent.Size changes to before #9018 was called. (#9679)
Kept newly added methods for binary compatibility but deprecated them all for removal.
The entire pose/eye/size system needs to be reevaluated and address some of Mojang's changes.
However this should fix any bugs that pulling that PR may of caused.
2023-07-21 23:44:01 -07:00
Kovak 93a615e900 Add a config option to restore the calculate all normals behavior in case some setups require old broken behavior. (#9670) 2023-07-19 14:46:05 -07:00
Kovak 7c84249996 Fix rounding errors on models with 45 degree normals by favoring one Direction (#9669)
Should fix flickering issues when breaking custom models and having our vanilla solution disabled.
2023-07-19 14:44:28 -07:00
RealMangoRage 881725c7d5 Moved ForgeHooksClient.onCreativeModeTabBuildContents to ForgeHooks to fix #9662 2023-07-19 11:18:45 -07:00
LexManos 2745531f08 Fix tag removal functionality that broke during the 1.19 update. Closes #9053 and #8949 2023-07-19 10:26:55 -07:00
LexManos bcdd44aece Replace string with forge tag in vanilla recipes. Closes #9062 2023-07-19 00:02:11 -07:00
LexManos bad5b1b5d4 Fix new brain hooks not persisting active activities. 2023-07-18 23:50:27 -07:00
Kovak 30f255d76c Fix breaking overlay flickering on campfires by using vanilla method to calculate block normals. (#9664) 2023-07-17 18:10:12 -07:00
LexManos ebb316a15c Cleanup usages of static imports and build script so that our build doesn't spam useless error like messages.
So that it is easier to see real errors.
Add compatibility checking to standard testing tasks.
2023-07-17 17:26:28 -07:00
LegoMaster3650 e6bb7eecbd Added Action value to PlayerInteractEvent.LeftClickEvent to expose what action fired the event. #9175 2023-07-17 10:57:10 -07:00
Jonathan Colmenares c66f70d6c2 Fix parameter names in IForgeDimensionSpecialEffects.adjustLightmapColors to better describe what they are. (#9656) 2023-07-17 09:38:05 -07:00
Jonathan Colmenares 26adf4da44 Re-add EntityEvent.Size methods to maintain bincompat 2023-07-17 07:50:18 -07:00
mgaray99 30d0a630d6 Added LivingMakeBrainEvent, to allow a consistent way for modders to manipulate entity Brains. #9292 2023-07-15 12:37:42 -07:00
JustRed23 00ad61b4ae Add LivingSwapHandItemsEvent 2023-07-15 11:15:57 -07:00
brandon3055 09bc73d5cd Fixed FluidUtil#tryFillContainer returning invalid result when simulating #9358 2023-07-15 10:51:17 -07:00
Jonathan Colmenares 1ee53c0464 Re-add in-game mod menu (#9652) 2023-07-15 10:45:34 -07:00
LexManos 16863c8d3b Fix Entity eye height for multipart entities. 2023-07-15 10:45:21 -07:00
Random832 150e8281ae Fix conflicting keybindings not having the correct click count set. #9360 2023-07-15 09:28:47 -07:00
Kovak 4385eb2155 Fix the Emissive Rendering for Experimental Light Pipeline (#9651) 2023-07-15 08:07:37 -07:00
RealMangoRage 5c40bef28e Fixed AdvancementsScreen.java.patch buttons not rendering (#9649) 2023-07-14 19:06:17 -07:00
ChampionAsh5357 55aa419100 Properly Handle Fluid Updates while in a Boat #9428 2023-07-14 19:05:27 -07:00
SilverDavid a4bef82836 New hook in IClientBlockExtensions to handle enabling tinting on breaking particles. #9446 2023-07-14 16:08:24 -07:00
LexManos b741df5f18 Fix invalid index when ticking itemstacks in a player nventory by adding a new onInventoryTick event. Closes #9453 2023-07-14 15:42:13 -07:00
LexManos 0c81a71308 Make the FireworkShapes enum extensible (Closes #9486) 2023-07-14 14:01:32 -07:00
Technici4n ea7f755a11 Add EmptyEnergyStorage (#9487) 2023-07-14 11:40:16 -07:00
Paint_Ninja d947d35eb1 Support IPv6 address compression for logged IPs 2023-07-14 11:14:56 -07:00
Sara Freimer 5271ef79fe Make item name rendering and status bar rendering respect additional gui overlays being rendered by mods (#9648) 2023-07-14 10:17:43 -07:00
Meldexun b7131824f0 Fix EyeHeight event being fired twice (#9647) 2023-07-14 10:09:48 -07:00
DBotThePony 3e624db4f8 Add PlayerSpawnPhantomsEvent, utilized to block or forcefully allow PhantomSpawner to spawn phantoms (#9644) 2023-07-14 07:20:21 -07:00
Nathan fc7af96bde Fix creative mode screen not correctly using CreativeModeTab::getTabsImage (#9627) 2023-07-14 07:10:49 -07:00
Paint_Ninja b1965db30b Add option to advertise dedicated servers to LAN. 2023-07-14 05:58:10 -07:00
LexManos 8df5a80725 Fix entity eye height loop. 2023-07-14 05:57:50 -07:00
ChampionAsh5357 d9043298d3 Particle Description Data Provider. 2023-07-13 19:34:53 -07:00
Meldexun e91ff9a5a6 Add LivingBreatheEvent and LivingDrownEvent (#9525) 2023-07-13 19:19:40 -07:00
Meldexun 77fb08713f Fix entity size event not being fired, changed it to split eye height and size calculations. (#9535) 2023-07-13 19:16:17 -07:00
bconlon 2c965fac90 AlterGroundEvent for modifying block placement performed by AlterGroundDecorator (#9637) 2023-07-13 17:29:03 -07:00
JustRed23 2f229c5bb1 Change ProjectileHitEvent to return a result instead of being cancelable. Closes #9642 2023-07-13 17:21:13 -07:00
noeppi_noeppi 54b08d2711 Replace static import with regular one to fix S2S and non-official mappings. (#9633) 2023-07-11 11:26:02 -07:00
Su5eD 46f641a502 Add missing null check for TagsProvider#existingFileHelper (#9638) 2023-07-11 11:25:39 -07:00
Sara Freimer 6304cb655b Add GuiGraphics helpers for blitNineSliced and blitRepeating that support specifying a custom texture size (#9641) 2023-07-10 22:03:47 +02:00
LexManos 27614b46a6 1.20.1 Recommended Build 2023-07-06 20:27:45 -07:00
LexManos ca47813bfa Fix FMLOnly loading. Closes #9609 2023-07-06 12:12:41 -07:00
Paint_Ninja b15b84fd59 Improve logging for server connections (#9618) 2023-07-05 15:01:18 -07:00
cech12 97edac430d Fix placing fluids into waterlogged blocks with FluidUtil::tryPlaceFluid. To behave more like vanilla MC-127110 (#9586) 2023-07-05 15:01:04 -07:00
Gegy 7b833faa46 Expose loaded RegistryAccess through AddReloadListenerEvent (#9613) 2023-07-05 15:00:12 -07:00
LexManos 93e4a7c649 Fix GLM applying to entities when killed. Closes #9551 2023-07-05 13:26:16 -07:00
Sara Freimer ccbc697bad Add unordered creative tabs after vanilla and perform a second level sorting of tabs by registry name to ensure tabs are ordered the same between game restarts (#9612) 2023-07-05 15:55:13 +02:00
cpw 77cd7043c9 Fix Early Loading window FPS snafu where it could spam (#9619)
unlimited screen updates. Probably a good way to gently toast an ancient laptop.

Signed-off-by: cpw <[email protected]>
2023-07-05 15:46:57 +02:00
Thomas Kain c0fe1688d8 Make overloads consistent for defineListAllowEmpty in ForgeConfigSpec.Builder (#9604) 2023-07-03 17:28:37 -07:00
LexManos 72e2ae1d6f Moved GameShuttingDownEvent hook to DedicatedServer class. Fixes #9601 2023-07-03 17:25:47 -07:00
LexManos 027f4d3d7b Fix PitcherCropBlock not calling canSustainPlant, not allowing it to be placed on custom farmland. Close #9611 2023-07-03 17:25:01 -07:00
LexManos 47ed2ad401 Add null check to NetworkHooks.openScreen. Closes #9597 2023-07-03 17:24:26 -07:00
dhyces b34275988c Fix ShieldBlockEvent not correctly performing damaged functions when not all damage is blocked. (#9615) 2023-07-03 16:56:22 -07:00
LexManos 47a2e137ed Add IP address to client disconnect message. Closes #9603 2023-07-03 16:54:40 -07:00
LexManos 063546c37d Fix hotbar items not dimming when sleeping in bed. Closes #9616 2023-07-03 16:54:31 -07:00
LexManos 3733005839 Bump Eventbus to address NullPointerException when classloading things on some threads. Closes #9570 2023-07-03 16:53:15 -07:00
cpw 7d0330eb08 Try and be a bit nicer about handling bad [feature] definitions (#9606)
* Try and be a bit nicer about handling bad [feature] definitions - they're single square bracket defined and require strings for feature bound values.

* Some more tweaks to the feature system to output useful and well formatted error messages when bounds fail. Requires SPI 7.0.1 and the language string "fml.modloading.feature.missing" has changed.

* Use immutable lists where possible and generally tidy things a bit
2023-06-28 22:27:59 -04:00
Marc Hermans 4c978a510c [HotFix]: Somehow not caught by CI, but there was an issue in the SpawnUtils to handle. 2023-06-27 15:20:08 +02:00
Sara Freimer 81f123fcab Remove amount from FluidStack hashCode to fix the equal/hashCode java contract (#9602) 2023-06-27 13:57:34 +02:00
Paint_Ninja a8dce04c1b Add example usage for configs to the MDK (#9596)
Demonstrates proper usage of different config value types, validation, transformation/parsing, load/reload handling and direct field access.
2023-06-27 13:57:20 +02:00
Paint_Ninja fa66faff6d Demonstrate configureEach in run configs (#9594) 2023-06-27 13:57:08 +02:00
Paint_Ninja 18536c76ae Reduce verbosity of prepareRuns doc in MDK (#9593) 2023-06-27 13:56:58 +02:00
Paint_Ninja ad9b14e59c Lazily configure MDK tasks, improve IDE support (#9592) 2023-06-27 13:56:42 +02:00
Sara Freimer 45e27c62e9 Fix not dropping xp for player sourced explosions and various other cases when xp should drop (#9588) 2023-06-27 13:56:28 +02:00
cech12 f1939ef144 add item handler capability to chiseled bookshelf (#9587) 2023-06-27 13:56:07 +02:00
Sara Freimer a43be39137 Fix ignoring maxUpStep method overrides on entities (#9583) 2023-06-27 13:55:52 +02:00
Sara Freimer 9450dfd67c Add missing damage type tag provider modid, existing file helper constructor overload (#9581) 2023-06-27 13:55:37 +02:00
Sara Freimer a3af55f85b Expose holder lookup registry provider as a helper in RegistriesDatapackGenerator (#9580) 2023-06-27 13:55:23 +02:00
Dennis C 9f858ef931 Fix gametest collection causing secondary crash in loading error state (#9568) 2023-06-27 13:55:06 +02:00
Sara Freimer b5f5d1d848 Fix SpriteCoordinateExpander not working with chained methods (MC-263524) (#9564) 2023-06-27 13:54:51 +02:00
Sara Freimer 15878af658 Expose EntityRendererProvider.Context to AddLayers event (#9562) 2023-06-27 13:54:41 +02:00
Brennan Ward 2d8d6b2a0a [1.20] Add events for checking spawn conditions (SpawnPlacementCheck and PositionCheck) (#9469) 2023-06-27 13:53:00 +02:00
cpw 7a25e83d34 Another tweak to the early display. We start a timer process while we create the window, in case it hangs. Also add a skip version config, to try and help in cases where the driver is stupid. (#9595) 2023-06-23 07:58:47 +02:00
Sara Freimer 8768c00f31 Auto generate names for modded loot pools. Fixes #9589 (#9591) 2023-06-22 11:56:47 -07:00
cpw 8c003dd965 More tweaks to the mod loading callbacks to make loading bars work better (#9585) 2023-06-22 20:36:09 +02:00
Sara Freimer dadc55cb2d Reimplement LootPool name patches and hooks (#9573) 2023-06-21 13:06:18 -07:00
Sara Freimer 83ab7656c9 Fix experimental lighting pipeline breaking vanilla's emissive rendering. Closes #9552 (#9582) 2023-06-21 20:30:36 +02:00
cpw d26cdf0b55 Update early loading default configs to match standard expectations. (#9577)
Also allow a global override env variable for darkmode always. "FML_EARLY_WINDOW_DARK"
2023-06-20 14:06:16 -07:00
ChiefArug e382ed526f Add proper duplicate mod error message that includes the mod id(s) and mod files. (#9474) 2023-06-20 14:05:38 -07:00
Dennis C 9cf1a77fdb Add missing stack tooltip rendering hooks (#9533)
Fix automatic tooltip wrapping discarding empty lines used as spacers
2023-06-20 14:05:06 -07:00
agnor99 5318d70c18 Add piston push reaction overrides to Block class (#9538) 2023-06-20 14:03:35 -07:00
Dennis C 060889656b Fix missing calls to level-sensitive block SoundType getter (#9553) 2023-06-20 14:02:43 -07:00
Sara Freimer c43c15932f Fix forge registries that have wrappers/support tags not being in the HolderLookup Provider that is provided to the GatherDataEvent (#9566) 2023-06-20 14:02:20 -07:00
Sara Freimer 4d9df706b0 Make IItemDecorator a functional interface again. Fixes #9563 (#9574) 2023-06-20 14:02:11 -07:00
LexManos af8900a748 Make 1.20.x the main TC branch. 2023-06-20 14:01:26 -07:00
Sara Freimer 439d07e403 Fix custom geometry in parent models not being resolved (#9572) 2023-06-20 17:36:22 +02:00
cpw 1ad8d48683 Make the scheduled thread factory setDaemon on it's threads. Should allow things to close properly if something deadly happens early on. (#9575) 2023-06-20 15:44:02 +02:00
cpw 031d008826 This is an early display window system for forge. (#9558) 2023-06-19 13:01:45 +02:00
cpw 551feaf019 fix the JIJ break by bumping SJH. apologies.
Signed-off-by: cpw <[email protected]>
2023-06-15 19:07:29 -04:00
cpw 032639b151 update libs (#9565)
Signed-off-by: cpw <[email protected]>
2023-06-14 22:30:36 -04:00
ChampionAsh5357 a32a06ce8a Update Armor Layer Implementation to Match Vanilla (#9547) 2023-06-12 19:20:07 -07:00
LexManos baa3098435 1.20.1 Update 2023-06-12 15:09:50 -07:00
SizableShrimp 1a413eeed2 Fix JAR compatibility checks for 1.20 (#9556) 2023-06-11 18:26:34 +02:00
SizableShrimp 497274fd93 [1.20] Add AFTER_LEVEL render level stage (#9555) 2023-06-11 12:28:55 +02:00
ChampionAsh5357 2eddcabdb7 Reorder overlay layers to match vanilla (#9550) 2023-06-10 12:01:33 +01:00
Dennis C ce84ba86f0 Re-implement missing level-sensitive block light hook in ChunkAccess (#9536) 2023-06-10 09:13:29 +01:00
Dennis C 622c47c075 Fix issues in the deserialization of empty ingredients (#9537) 2023-06-09 14:49:13 -07:00
agnor99 7614e024ed Fix wrong variable passed into EnchantmentClue lookup Fixes #9543 (#9544) 2023-06-09 14:48:53 -07:00
Dennis C a4570cb22f Fix incorrect depth test state in debug graph rendering Fixes #9534 (#9539) 2023-06-09 14:48:09 -07:00
ChampionAsh5357 76349744f7 Fix initCapabilities patch location in ServerLevel Fixes #9526 (#9531) 2023-06-09 14:47:38 -07:00
ChampionAsh5357 75d647615e Use Spinning Effect Intensity instead of Partial Tick for Portal Overlay Fixes #9529 (#9530) 2023-06-09 14:47:05 -07:00
Matei M 8482714375 Fix getArmorModelHook patch, Fixex #9523 (#9528) 2023-06-09 14:46:26 -07:00
Mgazul 2d8a935695 Fix duplicate Map writes in PlayerList patch. (#9521) 2023-06-09 14:45:57 -07:00
LexManos f6c13682d2 Fix Forge Version including MC version in MDK. 2023-06-09 14:45:07 -07:00
agnor99 18c3ea9625 Fix patch in light propagation (#9532) 2023-06-08 21:48:23 +01:00
SizableShrimp 09de4a2dcc Attempt to fix jar signing
Gradle 8 is stupid and doesn't (easily) allow in-place tasks, so a temporary fix has been made in ForgeGradle 6

Signed-off-by: SizableShrimp <[email protected]>
2023-06-07 22:17:25 -05:00
d8c84e2949 Forge 1.20
- Creative mode tabs are now a registry; the `BuildContents` event was renamed to `BuildCreativeModeTabContentsEvent` and moved it to its own class
- The pack format is now 15 for both resource packs and data packs
- `ScreenUtils` was deprecated in favor of a `GuiGraphics` extension
- Forge and the MDK were updated to Gradle 8 and FG6
- The Forge common config file was removed (it only contained the deprecated old fields for resource caching, which was removed in 1.19.3)
- Registry dummy entries were removed
- `RemappingVertexPipeline` was fixed to forward the `endVertex()` call
- Forge tool tags were removed in favor of vanilla ones

Co-authored-by: ChampionAsh5357 <[email protected]>
Co-authored-by: coehlrich <[email protected]>
Co-authored-by: Dennis C <[email protected]>
Co-authored-by: Matyrobbrt <[email protected]>
2023-06-07 20:55:09 -05:00
LexManos 658e614dc1 1.19.4 Recommended Build 2023-06-07 12:08:15 -07:00
Apex 9abfcbc90b Add method to GatherDataEvent to obtain collection of all input paths. (#9499) 2023-05-29 13:47:21 -07:00
Dennis C 6ee20899bf Log error when Sheets is class-loaded before registration is completed (#9475) 2023-05-29 13:47:04 -07:00
Dennis C 42115d37d6 [1.19.x] Re-implement RenderTooltipEvent.Color (#9497)
* Reimplement RenderTooltipEvent.Color

* Formatting, comments, EXC

* Deprecate instead of replacing
2023-05-16 12:24:27 +02:00
Joseph B 8abe9b6b0c Add API for registering custom world preset editors (#9436) 2023-05-14 15:53:34 -05:00
embeddedt fc91dfd1d1 Remove unneeded extra reload of datapacks on world creation screen (#9454) 2023-05-14 15:52:57 -05:00
SizableShrimp 8cd6f2d237 Bump ASM to 9.5 2023-05-14 15:50:07 -05:00
SizableShrimp f1702495ef Fix crash when running server from root directory
Fixes #9498
2023-05-14 15:26:24 -05:00
Dennis C 2ab2ced589 Fix root transform matrix format, allow using all four root transform formats (#9496) 2023-05-12 11:20:01 -07:00
Apex c8f9e59fef Add missing AT lines to allow registering custom game rule boolean/integer types (#9489) 2023-05-10 09:27:10 -07:00
Brennan WardandBrennan Ward 9a25527a54 [1.19.x] Fix SaplingGrowTreeEvent#setFeature being ignored in FungusBlock (#9485)
Co-authored-by: Brennan Ward <[email protected]>
2023-05-05 23:09:13 -05:00
SizableShrimp ad6d11671e Restore AccessibilityOnboardingScreen
Fixes #9488
2023-05-05 23:07:49 -05:00
Brennan Ward 3c93cd414d Update documentation on FinalizeSpawn (#9467) 2023-05-05 10:26:41 -07:00
DBotThePony 2c243626f6 Fix fluids without sound event causing exception in tryFillContainer and tryEmptyContainer (#9445) 2023-05-05 10:26:28 -07:00
Technici4n ae05036bb3 Make FakePlayerFactory respect the given ServerLevel (#9479) 2023-05-05 10:03:55 -07:00
Dennis C 3de22ffba3 Collect and log exceptions occurring in DeferredWorkQueue tasks (#9449) 2023-05-04 16:43:34 -07:00
matyrobbrt e1bf07dece Fix NamespacedWrapper#wrapAsHolder (#9450) 2023-05-04 16:42:02 -07:00
RealMangoRage 0ae63b0b07 Fixes ChatScreen calling .setScreen (#9443)
Fix test compile failures also.
2023-04-30 22:57:52 +01:00
matyrobbrt d4f211b815 Determine the Forge version the PR was built against when running PR compat checks (#9374) 2023-04-24 10:59:56 -07:00
SizableShrimp 393b28d7bb Add buildscript test to error on deprecated members that should of been removed. (#9460) 2023-04-24 10:59:37 -07:00
sciwhiz12 47c807c0e3 Remove erroneous brace patch in Inventory (#9462)
Fixes #9459
2023-04-23 13:24:20 +01:00
Dennis C b398f52a7f [1.19.4] Move root transform builder to ModelBuilder to allow use in ItemModelBuilder (#9456) 2023-04-20 09:27:30 +02:00
SizableShrimp f2aeeeb1fe Fix forge grindstone hooks allowing stacks of non-stackable items (#9457) 2023-04-20 00:19:35 -07:00
matyrobbrt dcef406df8 [1.19.4] Fix FMLOnly (#9415) 2023-04-18 16:46:04 +01:00
Dennis C 01846c729a Fix ItemLayerModel erroneously adding particle texture to layer texture list (#9441) 2023-04-07 08:16:17 +02:00
LexManos e6aaaff3f3 Temporary fix for Canceling ProjectileImpactEvents of Piercing ammo.
Event needs to be re-worked to have finer control. #9370
2023-04-05 11:16:39 -07:00
Brennan Ward d4836bc769 Fix dummy air blocks not being marked as air (#9440) 2023-04-03 15:04:34 -07:00
matyrobbrt b5655b0ddc Add support for splitting the login packet (#9367)
It contains full copies of data registries and can easily surpass vanilla's limits
2023-03-30 15:59:01 -07:00
Brennan Ward d75d333e13 Remove Attack Range and Reach Distance and add Block Reach and Entity Reach (#9361) 2023-03-29 18:53:41 -07:00
ChampionAsh5357 f5e16aa353 Add default bucket sounds for milk (#9432) 2023-03-29 18:48:29 -07:00
LexManos 4e5eecac12 Deprecate Item.onUsingTick, as vanilla provides the same function in Item.onUseTick now. Closes #9342 2023-03-29 18:46:00 -07:00
Brennan Ward a111073181 Fix ScreenEvent.Init.[Pre/Post] not working correctly (#9431) 2023-03-29 18:18:38 -07:00
LexManos a05f555ab8 Allow FenceGateBlock to be used without a WoodType. Closes #9392 2023-03-29 17:59:51 -07:00
LexManos df3db236e5 Deprecate duplicate tool tags that vanilla added in 1.19.4
We will maintain a seperate 'tools' tag until Mojang adds all relevent tool tags.
Closes #9393
2023-03-29 17:59:50 -07:00
LexManos 71b115c8fe Fix BlockEvent.Break not using ItemStack enchantment hooks. 2023-03-29 17:59:49 -07:00
LexManos 3e1b01af2c Move Block.onCatchFire to above block removal to allow usage of BlockEntity data. Closes #9400 2023-03-29 17:59:48 -07:00
Brennan Ward 1ab0685d04 Fix FinalizeSpawn not blocking spawns during worldgen (#9420) 2023-03-29 16:10:59 -07:00
LexManos 5c44992d1c Fixed issue with MutableHashedLinkedMap when removing multiple sequential entries in the middle of the map.
Added Unit tests for MutableHashLinkedMap
Added support for removing using the iterator
Added concurrent modification detection to the iterator
Added default constructor with basic hashing strategy.

Closes #9426
2023-03-29 15:49:49 -07:00
TelepathicGrunt d448ffd539 Loosen access for BucketItem's canBlockContainFluid (#9421) 2023-03-28 13:34:10 -07:00
ChampionAsh5357 9f895a79f2 Update and Regenerate Datapacks (#9419)
Add generation for pack.mcmeta
2023-03-28 13:33:59 -07:00
Paint_Ninja 6e1de81ff5 Restore ability to change message in ClientChatEvent (#9377) 2023-03-27 14:35:05 -07:00
sciwhiz12 b493a63fb6 Remove duplicate line in FoodData patch (#9424)
The line was accidentally duplicated in the 1.19.4 update and patching
process.

Fixes #9422
2023-03-27 12:51:53 -07:00
Joseph B ae65f5a255 Rename RegisterParticleProviderEvent's register methods to describe what kind of particle providers they register (deprecating old methods to avoid breaking) and minor docs tweaks (#9388) 2023-03-25 19:57:39 -07:00
noeppi_noeppi bb49b9e6dc Update pack versions (#9414) 2023-03-24 13:17:22 -07:00
Brennan Ward 2815e69407 [1.19.4] Revamp and fix spawn events (#9133) 2023-03-23 18:29:06 +00:00
Exopandora b33efc0727 [1.19.4] Replace blitOffset parameter with PoseStack in IItemDecorator (#9409)
* Replace blitOffset with PoseStack in IItemDecorator

* Circumvent breaking changes

* Fix blitOffset type
2023-03-22 11:11:01 +01:00
Dennis C 434c1ec4f8 Fix JSON model root transforms (#9410) 2023-03-22 11:09:41 +01:00
Dennis C c58d922324 Fix tossed items not being able to be picked up by other players. Closes #9412 (#9404) 2023-03-21 19:15:04 -07:00
Dennis C 331374dc7a Fix infinite BE render bounds failing frustum intersection test. Closes #9321 (#9407) 2023-03-21 12:16:49 -07:00
matyrobbrt c18eb23259 Make ForgeSlider use the new vanilla texture (#9406) 2023-03-20 14:16:35 -07:00
J.T. McQuigg d0e2242043 Add BlockSetType#register to accesstransformer.cfg (#9386) 2023-03-20 14:16:18 -07:00
Dennis C 684d36201f Add option to completely hide a crash-callable depending on a runtime value (#9372) 2023-03-20 14:15:59 -07:00
Jason 7b8ca60d32 Add isNewChunk to ChunkEvent.Load (#9369) 2023-03-20 14:15:33 -07:00
sciwhiz12 576ff81856 Remove DistExecutor calls in PistonEventTest (#9348) 2023-03-20 14:14:21 -07:00
sciwhiz12 73e68c9322 Fix hardcoded precipitation in ClimateSettingsBuilder (#9402)
This effectively caused all biomes to have precipitation, such as
minecraft:desert.

Fixes #9397
2023-03-20 08:56:10 +01:00
sciwhiz12 3cd758647d Fix incorrect variable used for swimming check (#9403)
Because of the incorrect variable, the check to stop sprinting (and stop
swimming) never fired correctly.

1.19.3's `flag5` variable was renamed to `flag7` in 1.19.4; however,
this was not caught during patching because of the fuzzy patcher.

Fixes #9399
2023-03-20 08:44:34 +01:00
sciwhiz12 14bc8d6611 Fix incorrect boolean used for glint effect (#9401)
The `flag1` variable is ultimately controlled by whether the armor slot
being rendered is for the leggings, which explains this bug where the
leggings always had the enchantment glint but not any other armor piece.

Fixes #9394
2023-03-20 08:02:18 +01:00
Redstone_Dubstep d552ce5eac Fixed ModMismatchDisconnectedScreen displaying missing mods wrongly (#9398) 2023-03-19 15:51:03 -07:00
Dennis C f03adc840e Fix misaligned text render type patch (#9391) 2023-03-16 14:44:20 -07:00
LexManos 31e73355c3 Remove thread filter from processing clientside custom payloads. Closes @9390 2023-03-16 11:51:46 -07:00
LexManos 88368dfa1c Fix LivingEntity patch which caused crash while entities got hurt. Closes #9389 2023-03-16 11:50:11 -07:00
matyrobbrt 7e6c8351c3 Fix wrong parameters in Screen#renderTooltipInternal patch (#9387) 2023-03-15 13:05:46 -07:00
LexManos fb20e4fa90 Fix misaligned patch in LevelRenderer. Closes #9385 2023-03-15 13:04:16 -07:00
Jonathan Coates 94c5699a03 Remove our fix for MC-121048 as it has been fixed by Vanilla (#9381) 2023-03-15 11:42:02 -07:00
LexManos 58626d1a22 Fix advancements not loading, bug seems to be fixed by vanilla now. Closes #9384 2023-03-15 11:41:12 -07:00
LexManos 631e72f99d Fixed patch verifier for cases where patches lowered access levels. Closes #9383 2023-03-15 11:39:42 -07:00
LexManos 3fffc18729 Fix crouching while sprinting stopping the player when step height is modified. Closes #9376 2023-03-14 23:56:29 -07:00
3277 changed files with 69737 additions and 65546 deletions

No files matched your search

+3
View File
@@ -21,3 +21,6 @@ src/generated/**/*.json text eol=lf
src/generated_test/**/.cache/cache text eol=lf
src/generated_test/**/*.json text eol=lf
# We force eclipse to use unix line endings
ide/eclipse/template/.settings/*.prefs text eol=lf
+28
View File
@@ -0,0 +1,28 @@
name: Publish
on:
push:
branches: [ '1.21.3' ]
permissions:
contents: read
jobs:
build:
uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@main
with:
java: 21
gradle_setup: './gradlew setup'
gradle_tasks: 'build publish'
artifact_name: 'forge'
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }}
PROMOTE_ARTIFACT_USERNAME: ${{ secrets.PROMOTE_ARTIFACT_USERNAME }}
PROMOTE_ARTIFACT_PASSWORD: ${{ secrets.PROMOTE_ARTIFACT_PASSWORD }}
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS }}
SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
SIGN_KEYSTORE_PASSWORD: ${{ secrets.SIGN_KEYSTORE_PASSWORD }}
SIGN_KEYSTORE_DATA: ${{ secrets.SIGN_KEYSTORE_DATA }}
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
name: Upload payload
runs-on: ubuntu-latest
steps:
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
with:
name: payload
path: ${{ github.event_path }}
@@ -7,7 +7,7 @@ jobs:
name: Upload payload
runs-on: ubuntu-latest
steps:
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
with:
name: payload
path: ${{ github.event_path }}
+9 -14
View File
@@ -13,6 +13,9 @@
/classes
*.iml
# We however WANT the ide folder as it has tempaltes/configs for developing Forge.
!ide/**
#gradle
/build
/.gradle
@@ -23,8 +26,6 @@
/projects/mcp/
/projects/clean/
/projects/forge/
/projects/fmlonly/
#/patches/*
#occupational hazards
/projects/**/build/
@@ -32,17 +33,11 @@
/projects/**/run/
/projects/**/*.launch
/repo/
src/generated/resources/.cache/
src/generated_test/resources/.cache/
src/*/generated/**/.cache/
#Patch rejects
/patches-/
*.patch.rej
/projects/*/rejects/
# FML changelog
changelog.txt
*.py
*.7z
/logs/
# Generated by gradle every import
**/src/main/resources/META-INF/MANIFEST.MF
/fmlloader/src/main/resources/forge_version.json
/ForgeRoot.ipr
/ForgeRoot.iws
-104
View File
@@ -1,104 +0,0 @@
<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<name>MinecraftForge_Forge Config DSL Script</name>
<groupId>MinecraftForge_Forge</groupId>
<artifactId>MinecraftForge_Forge_dsl</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<repositories>
<repository>
<id>jetbrains-all</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>teamcity-server</id>
<url>https://teamcity.minecraftforge.net/app/dsl-plugins-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>JetBrains</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>${basedir}</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration/>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>teamcity-configs-maven-plugin</artifactId>
<version>${teamcity.dsl.version}</version>
<configuration>
<format>kotlin</format>
<dstDir>target/generated-configs</dstDir>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin</artifactId>
<version>${teamcity.dsl.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-script-runtime</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
-208
View File
@@ -1,208 +0,0 @@
import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.githubIssues
import jetbrains.buildServer.configs.kotlin.*
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
/*
The settings script is an entry point for defining a TeamCity
project hierarchy. The script should contain a single call to the
project() function with a Project instance or an init function as
an argument.
VcsRoots, BuildTypes, Templates, and subprojects can be
registered inside the project using the vcsRoot(), buildType(),
template(), and subProject() methods respectively.
To debug settings scripts in command-line, run the
mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
command and attach your debugger to the port 8000.
To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View
-> Tool Windows -> Maven Projects), find the generate task node
(Plugins -> teamcity-configs -> teamcity-configs:generate), the
'Debug' option is available in the context menu for the task.
*/
version = "2021.2"
project {
buildType(Build)
buildType(BuildSecondaryBranches)
buildType(PullRequests)
buildType(PullRequestChecks)
buildType(PullRequestCompatibility)
params {
text("docker_jdk_version", "17", label = "Gradle version", description = "The version of the JDK to use during execution of tasks in a JDK.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("docker_gradle_version", "7.5.1", label = "Gradle version", description = "The version of Gradle to use during execution of Gradle tasks.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("git_main_branch", "1.19.x", label = "Git Main Branch", description = "The git main or default branch to use in VCS operations.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("git_branch_spec", """
+:refs/heads/(main*)
+:refs/heads/(master*)
+:refs/heads/(develop|release|staging|main|master)
+:refs/heads/(1.*)
""".trimIndent(), label = "The branch specification of the repository", description = "By default all main branches are build by the configuration. Modify this value to adapt the branches build.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
text("github_repository_name", "MinecraftForge", label = "The github repository name. Used to connect to it in VCS Roots.", description = "This is the repository slug on github. So for example `MinecraftForge` or `MinecraftForge`. It is interpolated into the global VCS Roots.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
text("env.PUBLISHED_JAVA_ARTIFACT_ID", "forge", label = "Published artifact id", description = "The maven coordinate artifact id that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_FML_ARTIFACT_ID", "fmlonly", label = "Published fmlonly artifact id", description = "The maven coordinate artifact id for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION", "0.0.0-SNAPSHOT", label = "Published fmlonly artifact version", description = "The version for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
text("env.PUBLISHED_JAVA_GROUP", "net.minecraftforge", label = "Published group", description = "The maven coordinate group that has been published by this build. Can not be empty.", allowEmpty = false)
//These are references and not actually keys
password("env.CROWDIN_KEY", "credentialsJSON:a3102dbe-805d-4177-9f54-3d2c2eb08fd5", display = ParameterDisplay.HIDDEN)
password("env.KEYSTORE_URL", "credentialsJSON:a7ae1c82-8058-4061-8d12-7f6bc2618d2e", display = ParameterDisplay.HIDDEN)
password("env.KEYSTORE_PASSWORD", "credentialsJSON:d7b964e3-a1fd-47a8-b892-6f601fe47479", display = ParameterDisplay.HIDDEN)
text("additional_publishing_gradle_parameters", "-PcrowdinKey=%env.CROWDIN_KEY% -PkeystoreKeyPass=%env.KEYSTORE_PASSWORD% -PkeystoreStorePass=%env.KEYSTORE_PASSWORD% -Pkeystore=%env.KEYSTORE_URL%", label = "Additional gradle parameters for publish", description = "Contains the additional gradle parameters used during publishing.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
checkbox("should_execute_build", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
checkbox("should_execute_test", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
}
features {
githubIssues {
id = "MinecraftForge_MinecraftForge__IssueTracker"
displayName = "MinecraftForge/MinecraftForge"
repositoryURL = "https://github.com/MinecraftForge/MinecraftForge"
}
}
}
object Build : BuildType({
templates(AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"), AbsoluteId("MinecraftForge_PublishProjectUsingGradle"), AbsoluteId("MinecraftForge_TriggersStaticFilesWebpageGenerator"))
id("MinecraftForge_MinecraftForge__Build")
name = "Build"
description = "Builds and Publishes the main branches of the project."
features {
feature {
id = "trigger_fml_only_files_generator"
type = "triggerBuildFeature"
param("triggers", "MinecraftForge_FilesGenerator_GeneratePages")
param("parameters", """
env.PUBLISHED_JAVA_FML_ARTIFACT_ID~env.PUBLISHED_JAVA_ARTIFACT_ID
env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION~env.PUBLISHED_JAVA_ARTIFACT_VERSION
env.PUBLISHED_JAVA_GROUP
""".trimIndent())
}
}
})
object BuildSecondaryBranches : BuildType({
templates(AbsoluteId("MinecraftForge_ExcludesBuildingDefaultBranch"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
id("MinecraftForge_MinecraftForge__BuildSecondaryBranches")
name = "Build - Secondary Branches"
description = "Builds and Publishes the secondary branches of the project."
params {
text("git_branch_spec", """
+:refs/heads/(*)
-:refs/heads/noci*
""".trimIndent(), label = "The branch specification of the repository", description = "By default all main branches are build by the configuration. Modify this value to adapt the branches build.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequests : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequests")
name = "Pull Requests"
description = "Builds pull requests for the project"
params {
checkbox("should_execute_build", "true", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
checked = "true", unchecked = "false")
text(
"gradle_build_task",
"assemble",
label = "Gradle build task to execute during build",
description = "Determines the build task that is executed to build the project.",
display = ParameterDisplay.HIDDEN,
allowEmpty = false
)
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequestChecks : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequestChecks")
name = "Pull Requests (Checks)"
description = "Checks pull requests for the project"
steps {
gradle {
name = "Check"
id = "RUNNER_10_Check"
tasks = "checkAll"
gradleParams = "--continue %gradle_custom_args%"
enableStacktrace = true
dockerImage = "%docker_gradle_image%"
dockerRunParameters = """
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
--network=host
-u 1000:1000
%docker_additional_args%
""".trimIndent()
}
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
object PullRequestCompatibility : BuildType({
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
id("MinecraftForge_MinecraftForge__PullRequestCompatibility")
name = "Pull Requests (Compatibility)"
description = "Validates binary compatibility for pull requests made to the project"
steps {
gradle {
name = "Validate"
id = "RUNNER_10_Compatibility"
tasks = "checkJarCompatibility"
gradleParams = "--continue %gradle_custom_args%"
enableStacktrace = true
dockerImage = "%docker_gradle_image%"
dockerRunParameters = """
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
--network=host
-u 1000:1000
%docker_additional_args%
""".trimIndent()
}
}
vcs {
branchFilter = """
+:*
-:1.*
-:<default>
""".trimIndent()
}
})
-65
View File
@@ -1,65 +0,0 @@
Minecraft Forge: Credits/Thank You
Forge is a set of tools and modifications to the Minecraft base game code to assist
mod developers in creating new and exciting content. It has been in development for
several years now, but I would like to take this time thank a few people who have
helped it along it's way.
First, the people who originally created the Forge projects way back in Minecraft
alpha. Eloraam of RedPower, and SpaceToad of Buildcraft, without their acceptiance
of me taking over the project, who knows what Minecraft modding would be today.
Secondly, someone who has worked with me, and developed some of the core features
that allow modding to be as functional, and as simple as it is, cpw. For developing
FML, which stabelized the client and server modding ecosystem. As well as the base
loading system that allows us to modify Minecraft's code as elegently as possible.
Mezz, who has stepped up as the issue and pull request manager. Helping to keep me
sane as well as guiding the community into creating better additions to Forge.
Searge, Bspks, Fesh0r, ProfMobious, and all the rest over on the MCP team {of which
I am a part}. For creating some of the core tools needed to make Minecraft modding
both possible, and as stable as can be.
On that note, here is some specific information of the MCP data we use:
* Minecraft Coder Pack (MCP) *
Forge Mod Loader and Minecraft Forge have permission to distribute and automatically
download components of MCP and distribute MCP data files. This permission is not
transitive and others wishing to redistribute the Minecraft Forge source independently
should seek permission of MCP or remove the MCP data files and request their users
to download MCP separately.
And lastly, the countless community members who have spent time submitting bug reports,
pull requests, and just helping out the community in general. Thank you.
--LexManos
=========================================================================
This is Forge Mod Loader.
You can find the source code at all times at https://github.com/MinecraftForge/MinecraftForge/tree/1.12.x/src/main/java/net/minecraftforge/fml
This minecraft mod is a clean open source implementation of a mod loader for minecraft servers
and minecraft clients.
The code is authored by cpw.
It began by partially implementing an API defined by the client side ModLoader, authored by Risugami.
http://www.minecraftforum.net/topic/75440-
This support has been dropped as of Minecraft release 1.7, as Risugami no longer maintains ModLoader.
It also contains suggestions and hints and generous helpings of code from LexManos, author of MinecraftForge.
http://www.minecraftforge.net/
Additionally, it contains an implementation of topological sort based on that
published at http://keithschwarz.com/interesting/code/?dir=topological-sort
It also contains code from the Maven project for performing versioned dependency
resolution. http://maven.apache.org/
It also contains a partial repackaging of the javaxdelta library from http://sourceforge.net/projects/javaxdelta/
with credit to it's authors.
Forge Mod Loader downloads components from the Minecraft Coder Pack
(http://mcp.ocean-labs.de/index.php/Main_Page) with kind permission from the MCP team.
Vendored
-125
View File
@@ -1,125 +0,0 @@
@Library('forge-shared-library')_
pipeline {
options {
disableConcurrentBuilds()
}
agent {
docker {
image 'gradle:7-jdk16'
//args '-v forgegc:/home/gradle/.gradle_cache/'
}
}
environment {
GRADLE_ARGS = '--no-daemon --console=plain' // No daemon for now as FG3 kinda derps. //'-Dorg.gradle.daemon.idletimeout=5000'
DISCORD_WEBHOOK = credentials('forge-discord-jenkins-webhook')
DISCORD_PREFIX = "Job: Forge Branch: ${BRANCH_NAME} Build: #${BUILD_NUMBER}"
JENKINS_HEAD = 'https://wiki.jenkins-ci.org/download/attachments/2916393/headshot.png'
}
stages {
stage('notify_start') {
when {
not {
changeRequest()
}
}
steps {
discordSend(
title: "${DISCORD_PREFIX} Started",
successful: true,
result: 'ABORTED', //White border
thumbnail: JENKINS_HEAD,
webhookURL: DISCORD_WEBHOOK
)
}
}
/*
stage('pull_cache') {
steps {
sh 'cp -r /home/gradle/.gradle_cache/jdks/ /home/gradle/.gradle/jdks/'
sh 'cp -r /home/gradle/.gradle_cache/wrapper/ /home/gradle/.gradle/wrapper/'
sh 'cp -r /home/gradle/.gradle_cache/caches/ /home/gradle/.gradle/caches/'
}
}
*/
stage('setup') {
steps {
withGradle {
sh './gradlew ${GRADLE_ARGS} --refresh-dependencies --continue setup'
}
script {
env.MYVERSION = sh(returnStdout: true, script: './gradlew :forge:properties -q | grep "^version:" | cut -d" " -f2').trim()
env.FMLONLY_VERSION = sh(returnStdout: true, script: './gradlew :fmlonly:properties -q | grep "^version:" | cut -d" " -f2').trim()
}
}
}
stage('changelog') {
when {
not {
changeRequest()
}
}
steps {
writeChangelog(currentBuild, 'build/changelog.txt')
}
}
stage('publish') {
when {
not {
changeRequest()
}
}
environment {
CROWDIN = credentials('forge-crowdin')
KEYSTORE = credentials('forge-jenkins-keystore-old')
KEYSTORE_KEYPASS = credentials('forge-jenkins-keystore-old-keypass')
KEYSTORE_STOREPASS = credentials('forge-jenkins-keystore-old-keypass')
}
steps {
withCredentials([usernamePassword(credentialsId: 'maven-forge-user', usernameVariable: 'MAVEN_USER', passwordVariable: 'MAVEN_PASSWORD')]) {
withGradle {
sh './gradlew ${GRADLE_ARGS} publish -PkeystoreKeyPass=${KEYSTORE_KEYPASS} -PkeystoreStorePass=${KEYSTORE_STOREPASS} -Pkeystore=${KEYSTORE} -PcrowdinKey=${CROWDIN}'
}
}
}
post {
success {
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:forge ${env.MYVERSION} latest")], propagate: false, wait: false
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:fmlonly ${env.FMLONLY_VERSION} latest")], propagate: false, wait: false
}
}
}
stage('test_publish_pr') { //Publish to local repo to test full process, but don't include credentials so it can't sign/publish to maven
when {
changeRequest()
}
environment {
CROWDIN = credentials('forge-crowdin')
}
steps {
withGradle {
sh './gradlew ${GRADLE_ARGS} publish -PcrowdinKey=${CROWDIN}'
}
}
}
}
post {
always {
script {
archiveArtifacts artifacts: 'projects/forge/build/libs/**/*.*', fingerprint: true, onlyIfSuccessful: true, allowEmptyArchive: true
if (env.CHANGE_ID == null) { // This is unset for non-PRs
discordSend(
title: "${DISCORD_PREFIX} Finished ${currentBuild.currentResult}",
description: '```\n' + getChanges(currentBuild) + '\n```',
successful: currentBuild.resultIsBetterOrEqualTo("SUCCESS"),
result: currentBuild.currentResult,
thumbnail: JENKINS_HEAD,
webhookURL: DISCORD_WEBHOOK
)
}
}
}
}
}
+3
View File
@@ -57,6 +57,9 @@ This software contains a partial repackaging of javaxdelta, a BSD licensed progr
binary differences and applying them, sourced from the subversion at http://sourceforge.net/projects/javaxdelta/
authored by genman, heikok, pivot.
The only changes are to replace some Trove collection types with standard Java collections, and repackaged.
This software includes the Monocraft font from https://github.com/IdreesInc/Monocraft/ for use in the early loading
display.
=========================================================================
+19 -1702
View File
File diff suppressed because it is too large. Load diff
+8 -7
View File
@@ -1,12 +1,13 @@
repositories {
maven { url = 'https://maven.minecraftforge.net/' }
mavenCentral()
mavenCentral()
}
dependencies {
implementation 'org.ow2.asm:asm:9.3'
implementation 'org.ow2.asm:asm-tree:9.3'
implementation 'net.minecraftforge:srgutils:0.4.+'
implementation 'commons-io:commons-io:2.8.0'
implementation 'com.google.code.gson:gson:2.10'
}
implementation 'org.ow2.asm:asm:9.9.1'
implementation 'org.ow2.asm:asm-tree:9.9.1'
implementation 'net.minecraftforge:srgutils:0.6.2'
implementation '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')
}
}
@@ -1,23 +1,22 @@
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.OutputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@CompileStatic
abstract class BytecodeFinder extends DefaultTask {
@InputFile abstract RegularFileProperty getJar()
@OutputFile abstract RegularFileProperty getOutput()
// 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") })
@@ -27,24 +26,13 @@ abstract class BytecodeFinder extends DefaultTask {
protected void exec() {
Util.init()
def outputFile = output.get().asFile
var outputFile = output.get().asFile
if (outputFile.exists())
outputFile.delete()
pre()
jar.get().asFile.withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
ZipEntry zein
while ((zein = zin.nextEntry) != null) {
if (zein.name.endsWith('.class')) {
def node = new ClassNode(Opcodes.ASM9)
new ClassReader(zin).accept(node, 0)
process(node)
}
}
}
}
Util.processClassNodes(jar.get().asFile, this.&process)
post()
outputFile.text = new JsonBuilder(getData()).toPrettyString()
@@ -52,8 +40,8 @@ abstract class BytecodeFinder extends DefaultTask {
protected process(ClassNode node) {
if (node.fields != null) node.fields.each { process(node, it) }
if (node.methods != null) node.methods.each { process(node, it) }
if (node.fields !== null) node.fields.each { process(node, it) }
if (node.methods !== null) node.methods.each { process(node, it) }
}
protected pre() {}
@@ -0,0 +1,48 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@CompileStatic
abstract class BytecodePredicateFinder extends BytecodeFinder {
@Internal
abstract Property<Closure<Boolean>> getPredicate()
private final Map<String, List<String>> matches = new TreeMap<>(Comparator.naturalOrder())
@Override
protected process(ClassNode parent, MethodNode node) {
for (final current : node.instructions) {
if (predicate.get().call(parent, node, current)) {
var methods = matches.compute(parent.name, { k, v -> v ?: new ArrayList<>() })
// only add non-synthetic methods
if ((node.access & Opcodes.ACC_SYNTHETIC) == 0) {
methods.add(node.name + node.desc)
}
return
}
}
}
@Internal
@Override
protected Object getData() {
var array = new ArrayList<Object>()
matches.forEach { c, m ->
{
array.add(
'class': c,
'methods': m
)
}
}
return array ?: { throw new RuntimeException('Failed to find any targets, please ensure that method names and descriptors are correct.') }()
}
}
@@ -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;
}
}
@@ -1,5 +1,7 @@
package net.minecraftforge.forge.tasks
import groovy.transform.stc.FirstParam
import java.util.function.BiConsumer
public class ClosureHelper {
@@ -22,7 +24,7 @@ public class ClosureHelper {
}
}
static <T> T apply(T obj, Closure cl) {
static <T> T apply(T obj, @DelegatesTo(value = FirstParam, strategy = Closure.DELEGATE_FIRST) Closure cl) {
cl.delegate = obj
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
@@ -1,73 +0,0 @@
package net.minecraftforge.forge.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
abstract class Crowdin extends DefaultTask {
@Input abstract Property<String> getId()
@Input @Optional abstract Property<String> getKey()
@Input boolean json = true
@OutputFile abstract RegularFileProperty getOutput()
@OutputFile abstract RegularFileProperty getExport()
Crowdin() {
outputs.upToDateWhen{ false }
id.convention('minecraft-forge')
output.convention(project.layout.buildDirectory.dir(name).map {it.file("output.zip") })
export.convention(project.layout.buildDirectory.dir(name).map {it.file("export.json") })
}
@TaskAction
def run() {
File outputFile = output.get().asFile
File exportFile = export.get().asFile
if (outputFile.exists())
outputFile.delete()
if (!key.isPresent())
return
String key = this.key.get()
String id = this.id.get()
// Force an export
new URL("https://api.crowdin.com/api/project/${id}/export?key=${key}").withInputStream { i ->
exportFile.withOutputStream { it << i }
}
if (!exportFile.text.contains('success')) {
throw new RuntimeException("Crowdin export failed, see ${exportFile} for more info")
}
new URL("https://api.crowdin.com/api/project/${id}/download/all.zip?key=${key}").withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
outputFile.withOutputStream { out ->
new ZipOutputStream(out).withCloseable { zout ->
ZipEntry zein
while ((zein = zin.nextEntry) != null) {
if (zein.isDirectory()) {
zout.putNextEntry(new ZipEntry(zein.name))
} else {
// 1.13+ uses json
if (zein.name.endsWith('.json') == json) {
ZipEntry zeout = new ZipEntry(json ? zein.name.toLowerCase() : zein.name)
zeout.time = 1
zout.putNextEntry(zeout)
zout << zin
}
}
}
}
}
}
}
}
}
@@ -27,7 +27,7 @@ abstract class DownloadLibraries extends DefaultTask {
def run() {
Util.init()
File outputDir = output.get().asFile
def libraries = new ArrayList()
var libraries = new ArrayList<String>()
def json = input.get().asFile.json().libraries.each { lib ->
//TODO: Thread?
@@ -35,7 +35,7 @@ abstract class DownloadLibraries extends DefaultTask {
artifacts.each{ art ->
def target = new File(outputDir, art.path)
libraries.add(target.absolutePath)
if (!target.exists() || !art.sha1.equals(target.sha1())) {
if (!target.exists() || art.sha1 != target.sha1()) {
project.logger.lifecycle("Downloading ${art.url}")
if (!target.parentFile.exists()) {
target.parentFile.mkdirs()
@@ -43,7 +43,7 @@ abstract class DownloadLibraries extends DefaultTask {
new URL(art.url).withInputStream { i ->
target.withOutputStream { it << i }
}
if (!art.sha1.equals(target.sha1())) {
if (art.sha1 != target.sha1()) {
throw new IllegalStateException("Failed to download ${art.url} to ${target.canonicalPath} SHA Mismatch")
}
}
@@ -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)
}
}
}
@@ -1,20 +1,11 @@
package net.minecraftforge.forge.tasks
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import java.util.ArrayList
import java.util.HashMap
import java.util.TreeMap
import java.util.TreeSet
import java.util.function.BiConsumer
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldNode
import org.objectweb.asm.tree.MethodNode
import static org.objectweb.asm.Opcodes.*
@@ -34,17 +25,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
@Override
protected process(ClassNode parent, MethodNode node) {
def last = null
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)) {
if (current.opcode === IF_ACMPEQ || current.opcode === IF_ACMPNE) {
if (last !== null && (last.opcode === GETSTATIC || last.opcode === GETFIELD)) {
def target = new Search(cls: last.owner, name: last.name)
def wanted = fieldsReverse.get(target)
def original = fields.get(wanted)
def instance = new ObjectTarget(owner: parent.name, name: node.name, desc: node.desc)
if (wanted != null && (original.blacklist == null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
if (wanted !== null && (original.blacklist === null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
targets.computeIfAbsent(wanted, { k -> new TreeSet() }).add(instance)
}
}
@@ -68,9 +59,10 @@ abstract class FieldCompareFinder extends BytecodeFinder {
}
return ret
}
@CompileStatic
@EqualsAndHashCode(excludes = ['replacement', 'blacklist'])
public static class Search {
static class Search {
@Input
String cls
@@ -89,17 +81,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
return cls + '.' + name
}
def blacklist(def owner, def name, def desc) {
if (blacklist == null)
blacklist = new HashSet()
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(def owner) {
def blacklist(String owner) {
blacklist(owner, '', '')
}
}
def fields(Closure cl) {
void fields(Closure cl) {
new ClosureHelper(cl, {name, ccl ->
def search = ClosureHelper.apply(new Search(), ccl)
this.fields.put(name, search)
@@ -1,44 +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.provider.SetProperty
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import groovy.json.JsonSlurper
abstract class InstallerJar extends Zip {
@Input @Optional abstract SetProperty<String> getPackedDependencies()
@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)
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 = 'exclude'
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
if (!System.env.MAVEN_USER) {
dependsOn(project.universalJar)
from(project.universalJar) {
into '/maven/' + project.group.replace('.', '/') + '/' + project.universalJar.archiveBaseName.get() + '/' + project.version + '/'
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)
}
packedDependencies.get().forEach {
def jarTask = project.rootProject.getTasks().findByPath(it)
dependsOn(jarTask)
from(jarTask) {
into '/maven/' + jarTask.project.group.replace('.', '/') + '/' + jarTask.archiveBaseName.get() + '/' + jarTask.project.version + '/'
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" }
}
}
}
}
}
@@ -7,13 +7,13 @@ import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.nio.file.Files
abstract class InstallerJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input @Optional abstract SetProperty<String> getPackedDependencies()
@Input @Optional final Map<String, Object> libraries = new LinkedHashMap<>()
@Input Map<String, Object> json = new LinkedHashMap<>()
@InputFile abstract RegularFileProperty getIcon()
@@ -23,29 +23,19 @@ abstract class InstallerJson extends DefaultTask {
@Input abstract Property<String> getWelcome()
InstallerJson() {
getLauncherJsonName().convention('/version.json')
getLogo().convention('/big_logo.png')
getMirrors().convention('https://files.minecraftforge.net/mirrors-2.0.json')
getWelcome().convention("Welcome to the simple ${project.name.capitalize()} installer.")
getOutput().convention(project.layout.buildDirectory.file('install_profile.json'))
['client', 'server'].each { side ->
['slim', 'extra'].each { type ->
def tsk = project.tasks.getByName("download${side.capitalize()}${type.capitalize()}")
dependsOn(tsk)
input.from(tsk.output)
}
def tsk = project.tasks.getByName("create${side.capitalize()}SRG")
dependsOn(tsk)
input.from(tsk.output)
}
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 {
(packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]).forEach {
dependsOn(it)
input.from it.archiveFile
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { packed ->
dependsOn(packed)
input.from packed.archiveFile
}
}
}
@@ -53,17 +43,19 @@ abstract class InstallerJson extends DefaultTask {
@TaskAction
protected void exec() {
def libs = libraries
for (def child : packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]) {
def dep = Util.getMavenDep(child)
def path = Util.getMavenPath(child)
libs.put(dep.toString(), [
name: dep,
[
project.tasks.universalJar,
project.tasks.serverShimJar
].forEach { AbstractArchiveTask packed ->
def info = Util.getMavenInfoFromTask(packed)
libs.put(info.name, [
name: info.name,
downloads: [
artifact: [
path: path,
url: "https://maven.minecraftforge.net/${path}",
sha1: child.archiveFile.get().asFile.sha1(),
size: child.archiveFile.get().asFile.length()
path: info.path,
url: "https://maven.minecraftforge.net/$info.path",
sha1: packed.archiveFile.get().asFile.sha1(),
size: packed.archiveFile.get().asFile.length()
]
]
])
@@ -6,6 +6,7 @@ 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
@@ -16,59 +17,80 @@ abstract class LauncherJson extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutput()
@InputFiles abstract ConfigurableFileCollection getInput()
@Input Map<String, Object> json = new LinkedHashMap<>()
@Input @Optional abstract SetProperty<String> getPackedDependencies()
@Internal final vanilla = project.project(':mcp').file('build/mcp/downloadJson/version.json')
@Internal final timestamp = iso8601Now()
@Internal final comment = [
"Please do not automate the download and installation of Forge.",
"Our efforts are supported by ads from the download page.",
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
]
@Internal final id = "${project.rootProject.ext.MC_VERSION}-${project.name}${project.version.substring(project.rootProject.ext.MC_VERSION.length())}"
LauncherJson() {
getOutput().convention(project.layout.buildDirectory.file('version.json'))
output.convention(project.layout.buildDirectory.file('libs/version.json'))
dependsOn(':fmlloader:jar', 'universalJar')
getInput().from(project.tasks.universalJar.archiveFile,
project.project(':fmlloader').jar.archiveFile,
vanilla)
project.afterEvaluate {
packedDependencies.get().forEach {
def jarTask = project.rootProject.tasks.findByPath(it)
dependsOn(jarTask)
input.from jarTask.archiveFile
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() {
if (!json.libraries)
json.libraries = []
def libs = [:]
getArtifacts(project, project.configurations.installer, false).each { key, lib -> libs[key] = lib }
getArtifacts(project, project.configurations.moduleonly, false).each { key, lib -> libs[key] = lib }
packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) }.forEach {
def path = Util.getMavenPath(it)
def key = Util.getMavenDep(it)
libs[key] = [
name: key,
downloads: [
artifact: [
path: path,
url: "https://maven.minecraftforge.net/${path}",
sha1: it.archiveFile.get().asFile.sha1(),
size: it.archiveFile.get().asFile.length()
]
var 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()
]
]
}
libs.each { key, lib -> json.libraries.add(lib) }
])
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())
}
}
@@ -1,5 +1,6 @@
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
@@ -12,6 +13,7 @@ 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') })
@@ -24,8 +26,8 @@ abstract class MergeJars extends DefaultTask {
try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output.get().asFile))) {
for (def jar : jars) {
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(jar))) {
def entry
while ((entry = zin.getNextEntry()) != null) {
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)
@@ -27,6 +27,6 @@ public class ObjectTarget implements Comparable<ObjectTarget> {
@Override
int compareTo(ObjectTarget o) {
return toString().compareTo(o.toString())
return toString() <=> o.toString()
}
}
@@ -16,7 +16,7 @@ abstract class SetupCheckJarCompatibility extends DefaultTask {
SetupCheckJarCompatibility() {
group = 'jar compatibility'
onlyIf {
inputVersion.getOrNull() != null
inputVersion.getOrNull() !== null
}
outputs.upToDateWhen { false } // Never up to date, because this setup task should always run
@@ -26,16 +26,6 @@ abstract class SetupCheckJarCompatibility extends DefaultTask {
@TaskAction
void run() {
def inputVersion = inputVersion.get()
def fmlLibs = project.configurations.detachedConfiguration(project.PACKED_DEPS.collect {
def artifactId = it.split(':')[1]
return project.dependencies.create("net.minecraftforge:${artifactId}:${inputVersion}")
}.toArray(Dependency[]::new))
project.tasks.named('checkJarCompatibility') {
baseLibraries.from(project.provider {
fmlLibs.resolvedConfiguration.lenientConfiguration.files
})
}
def baseForgeUserdev = project.layout.buildDirectory.dir(name).map { it.file("forge-${inputVersion}-userdev.jar") }.get().asFile
project.rootProject.extensions.download.run {
@@ -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
}
}
@@ -2,149 +2,229 @@ 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.io.File
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.Date
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Semaphore
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
public class Util {
static void init() {
File.metaClass.sha1 = { ->
MessageDigest md = MessageDigest.getInstance('SHA-1')
delegate.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size)
}
return md.digest().collect {String.format "%02x", it}.join()
}
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.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 = { ->
def format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
def result = format.format(delegate)
return result[0..21] + ':' + result[22..-1]
}
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 ->
def lst = new ArrayList()
def x = 0, idx
def tmp = delegate
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit == -1 || x++ < limit)) {
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
}
}
public static String[] getClasspath(project, libs, artifact) {
def ret = []
artifactTree(project, artifact).each { key, lib ->
libs[lib.name] = lib
if (lib.name != artifact)
ret.add(lib.name)
}
return ret
}
public static def getArtifacts(project, config, classifiers) {
def ret = [:]
config.resolvedConfiguration.resolvedArtifacts.each {
def art = [
group: it.moduleVersion.id.group,
name: it.moduleVersion.id.name,
version: it.moduleVersion.id.version,
classifier: it.classifier,
extension: it.extension,
file: it.file
]
def key = art.group + ':' + art.name
def folder = "${art.group.replace('.', '/')}/${art.name}/${art.version}/"
def filename = "${art.name}-${art.version}"
if (art.classifier != null)
filename += "-${art.classifier}"
filename += ".${art.extension}"
def path = "${folder}${filename}"
def url = "https://libraries.minecraft.net/${path}"
if (!checkExists(url)) {
url = "https://maven.minecraftforge.net/${path}"
}
//TODO remove when Mojang launcher is updated
if (!classifiers && art.classifier != null) {
//Mojang launcher doesn't currently support classifiers, so... move it to part of the version, and force the extension to 'jar'
// However, keep the path normal so that our mirror system works.
art.version = "${art.version}-${art.classifier}"
art.classifier = null
art.extension = 'jar'
}
ret[key] = [
name: "${art.group}:${art.name}:${art.version}" + (art.classifier == null ? '' : ":${art.classifier}") + (art.extension == 'jar' ? '' : "@${art.extension}"),
downloads: [
artifact: [
path: path,
url: url,
sha1: sha1(art.file),
size: art.file.length()
]
]
]
}
return ret
}
public static def getMavenPath(task) {
def classifier = task.archiveClassifier.get()
def dep = "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
return "${task.project.group.replace('.', '/')}/${task.project.name}/${task.project.version}/${task.project.name}-${task.project.version}".toString() + (classifier == '' ? '' : '-' + classifier) + '.jar'
}
public static def getMavenDep(task) {
def classifier = task.archiveClassifier.get()
return "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
static String[] getClasspath(Project project, Map libs, String artifact) {
def ret = []
artifactTree(project, artifact).each { key, lib ->
libs[lib.name] = lib
if (lib.name != artifact)
ret.add(lib.name)
}
return ret
}
public static def iso8601Now() { new Date().iso8601() }
@CompileStatic
static Map getArtifacts(Configuration config) {
var ret = [:]
var semaphore = new Semaphore(1, true)
config.resolvedConfiguration.resolvedArtifacts.parallelStream().forEachOrdered(dep -> {
var info = getMavenInfoFromDep(dep)
var domain = 'libraries.minecraft.net'
var url = "https://$domain/$info.path"
if (!checkExists(url))
url.values[0] = 'maven.minecraftforge.net'
public static def sha1(file) {
MessageDigest md = MessageDigest.getInstance('SHA-1')
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size)
}
return md.digest().collect {String.format "%02x", it}.join()
}
var sha1 = sha1(dep.file)
private static def artifactTree(project, artifact) {
if (!project.ext.has('tree_resolver'))
project.ext.tree_resolver = 1
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
def dep = project.dependencies.create(artifact)
cfg.dependencies.add(dep)
def files = cfg.resolve()
return getArtifacts(project, cfg, true)
}
private static boolean checkExists(url) {
try {
def code = new URL(url).openConnection().with {
requestMethod = 'HEAD'
connect()
responseCode
}
return code == 200
} catch (Exception e) {
if (e.toString().contains('unable to find valid certification path to requested target'))
throw new RuntimeException('Failed to connect to ' + url + ': Missing certificate root authority, try updating java')
throw e
}
}
public static String getLatestForgeVersion(mcVersion) {
def json = new JsonSlurper().parseText(new URL("https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json").getText("UTF-8"))
return json.promos["$mcVersion-latest"]
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
}
}
@@ -171,7 +171,7 @@ abstract class CheckATs extends CheckTask {
}
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance, IMappingFile mappings) {
final data = []
final data = [] as List<String>
final remapComment = { ATParser.Entry entry ->
if (!mappings || !entry || !entry.desc) return null
final comment = entry.comment?.substring(1)?.trim()
@@ -1,5 +1,6 @@
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
@@ -7,7 +8,6 @@ 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.Opcodes
import org.objectweb.asm.Type
import java.util.zip.ZipEntry
@@ -26,12 +26,12 @@ abstract class CheckExcs extends CheckTask {
excs.each { f ->
final lines = []
f.eachLine { line ->
def idx = line.indexOf('#')
if (idx == 0 || line.isEmpty()) {
int idx = line.indexOf('#')
if (idx === 0 || line.isEmpty()) {
return
}
if (idx != -1) line = line.substring(0, idx - 1)
if (idx !== -1) line = line.substring(0, idx - 1)
if (!line.contains('=')) {
reporter.report("Invalid: $line")
@@ -58,6 +58,8 @@ abstract class CheckExcs extends CheckTask {
return
}
lines.add(line)
return
}
if (fix) f.text = lines.sort().join('\n')
@@ -67,7 +69,7 @@ abstract class CheckExcs extends CheckTask {
private void collectKnown(Collection<String> known) {
binary.get().asFile.withInputStream { i ->
new ZipInputStream(i).withCloseable { zin ->
final visitor = new ClassVisitor(Opcodes.ASM9) {
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) {
@@ -6,4 +6,6 @@ import groovy.transform.CompileStatic
enum CheckMode {
CHECK,
FIX
CheckMode() {}
}
@@ -35,9 +35,14 @@ abstract class CheckPatches extends CheckTask {
}
}
}
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) {
final oldFixedErrors = reporter.fixed.size()
int oldFixedErrors = reporter.fixed.size()
final lines = Files.readAllLines(patch)
@@ -87,7 +92,7 @@ abstract class CheckPatches extends CheckTask {
pMatcher.group(6) == cMatcher.group(6) && // = ...
pMatcher.group(5) == cMatcher.group(5) && // field name
pMatcher.group(3) == cMatcher.group(3) && // static
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
(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)
}
@@ -98,7 +103,7 @@ abstract class CheckPatches extends CheckTask {
pMatcher.group(6) == cMatcher.group(6) && // params
pMatcher.group(5) == cMatcher.group(5) && // <T> void name
pMatcher.group(3) == cMatcher.group(3) && // static
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
(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)
}
@@ -109,7 +114,7 @@ abstract class CheckPatches extends CheckTask {
pMatcher.group(6) == cMatcher.group(6) && // ClassName<> extends ...
pMatcher.group(5) == cMatcher.group(5) && // class | interface
pMatcher.group(3) == cMatcher.group(3) && // static
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
(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)
}
}
@@ -135,7 +140,7 @@ abstract class CheckPatches extends CheckTask {
final nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-')
if (!prevLineChange && !nextLineChange) {
reporter.report("Patch contains white space change in valid hunk at line ${i + 1}, file: $patchPath", false)
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)
}
}
@@ -94,7 +94,7 @@ abstract class CheckSAS extends CheckTask {
}
}
protected static isSided(Annotatable annotatable) {
protected static boolean isSided(Annotatable annotatable) {
if (annotatable === null) return false
for (ann in annotatable.annotations) {
if ('Lnet/minecraftforge/api/distmarker/OnlyIn;' == ann.desc)
@@ -104,7 +104,7 @@ abstract class CheckSAS extends CheckTask {
}
protected static findChildMethods(Map<String, InheritanceData> json, String cls, String desc) {
return json.values().findAll{ it.methods != null && it.methods[desc] != null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
return json.values().findAll{ it.methods !== null && it.methods[desc] !== null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
.collect { it.name + ' ' + desc.replace(' ', '') } as TreeSet
}
}
@@ -1,6 +1,8 @@
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
@@ -33,10 +35,10 @@ abstract class CheckTask extends DefaultTask implements VerificationTask {
void run() {
Util.init()
final doFix = getMode().get() === CheckMode.FIX
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'))
@@ -88,21 +90,24 @@ abstract class CheckTask extends DefaultTask implements VerificationTask {
}
static <T extends CheckTask> void registerTask(TaskContainer tasks, String taskName, @DelegatesTo.Target('type') Class<T> clazz,
@DelegatesTo(genericTypeIndex = 0, target = 'type') Closure configuration) {
@DelegatesTo(genericTypeIndex = 0, target = 'type')
@ClosureParams(ThirdParam.FirstGenericType) Closure configuration) {
taskName = taskName.capitalize()
tasks.register("check$taskName", clazz) { CheckTask task ->
configuration.setDelegate((T) task)
configuration.call(task)
task.mode.set(CheckMode.CHECK)
task.group = 'checks'
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 ->
configuration.setDelegate((T) task)
configuration.call(task)
task.mode.set(CheckMode.FIX)
task.group = 'checks'
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'
}
+1077
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
}
}
+3 -1
View File
@@ -7,7 +7,7 @@ Contributing to Forge
3) Follow the code style of the class you're working in (braces on newlines & spaces instead of tabs in Forge classes, inline brackets in patches, etc).
For more information, refer to [the wiki](https://github.com/MinecraftForge/MinecraftForge/wiki/If-you-want-to-contribute-to-Forge)
For more information, refer to [the wiki][Wiki].
Contributor License Agreement
@@ -16,3 +16,5 @@ Contributor License Agreement
- You assign copyright ownership of your contributions to the patches codebase (everything under patches) to Forge, where it will be licensed under the LGPLv2.1 license.
"Forge" is Forge Development LLC, a legally incorporated entity in Oregon, USA.
[Wiki]: https://github.com/MinecraftForge/MinecraftForge/wiki/If-you-want-to-contribute-to-Forge
+48 -19
View File
@@ -2,16 +2,15 @@
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.19.3-&query=$.promos["1.19.3-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)](https://files.minecraftforge.net)
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=1.19.3-&query=$.promos["1.19.3-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
)](https://files.minecraftforge.net) [![Discord](https://img.shields.io/discord/313125603924639766.svg?color=%237289da&label=Discord&logo=discord&logoColor=%237289da)](https://discord.gg/UvedJ9m) [![Support](https://img.shields.io/badge/Patreon-Support-orange.svg?logo=Patreon)](https://www.patreon.com/LexManos)
[![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.3-&query=$.promos["1.21.3-recommended"]&color=brightgreen&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc3RhciBmYS13LTE4IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZGF0YS1pY29uPSJzdGFyIiBkYXRhLXByZWZpeD0iZmFzIiBmb2N1c2FibGU9ImZhbHNlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCA1NzYgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMjU5LjMgMTcuOEwxOTQgMTUwLjIgNDcuOSAxNzEuNWMtMjYuMiAzLjgtMzYuNyAzNi4xLTE3LjcgNTQuNmwxMDUuNyAxMDMtMjUgMTQ1LjVjLTQuNSAyNi4zIDIzLjIgNDYgNDYuNCAzMy43TDI4OCA0MzkuNmwxMzAuNyA2OC43YzIzLjIgMTIuMiA1MC45LTcuNCA0Ni40LTMzLjdsLTI1LTE0NS41IDEwNS43LTEwM2MxOS0xOC41IDguNS01MC44LTE3LjctNTQuNkwzODIgMTUwLjIgMzE2LjcgMTcuOGMtMTEuNy0yMy42LTQ1LjYtMjMuOS01Ny40IDB6IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)][Download]
[![Latest Release](https://img.shields.io/badge/dynamic/json?url=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json&label=Latest&prefix=1.21.3-&query=$.promos["1.21.3-latest"]&color=blue&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtYnVnIGZhLXctMTYiIGFyaWEtaGlkZGVuPSJ0cnVlIiBkYXRhLWljb249ImJ1ZyIgZGF0YS1wcmVmaXg9ImZhcyIgZm9jdXNhYmxlPSJmYWxzZSIgcm9sZT0iaW1nIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUxMS45ODggMjg4LjljLS40NzggMTcuNDMtMTUuMjE3IDMxLjEtMzIuNjUzIDMxLjFINDI0djE2YzAgMjEuODY0LTQuODgyIDQyLjU4NC0xMy42IDYxLjE0NWw2MC4yMjggNjAuMjI4YzEyLjQ5NiAxMi40OTcgMTIuNDk2IDMyLjc1OCAwIDQ1LjI1NS0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDBsLTU0LjczNi01NC43MzZDMzQ1Ljg4NiA0NjcuOTY1IDMxNC4zNTEgNDgwIDI4MCA0ODBWMjM2YzAtNi42MjctNS4zNzMtMTItMTItMTJoLTI0Yy02LjYyNyAwLTEyIDUuMzczLTEyIDEydjI0NGMtMzQuMzUxIDAtNjUuODg2LTEyLjAzNS05MC42MzYtMzIuMTA4bC01NC43MzYgNTQuNzM2Yy0xMi40OTggMTIuNDk3LTMyLjc1OSAxMi40OTYtNDUuMjU2IDAtMTIuNDk2LTEyLjQ5Ny0xMi40OTYtMzIuNzU4IDAtNDUuMjU1bDYwLjIyOC02MC4yMjhDOTIuODgyIDM3OC41ODQgODggMzU3Ljg2NCA4OCAzMzZ2LTE2SDMyLjY2NkMxNS4yMyAzMjAgLjQ5MSAzMDYuMzMuMDEzIDI4OC45LS40ODQgMjcwLjgxNiAxNC4wMjggMjU2IDMyIDI1Nmg1NnYtNTguNzQ1bC00Ni42MjgtNDYuNjI4Yy0xMi40OTYtMTIuNDk3LTEyLjQ5Ni0zMi43NTggMC00NS4yNTUgMTIuNDk4LTEyLjQ5NyAzMi43NTgtMTIuNDk3IDQ1LjI1NiAwTDE0MS4yNTUgMTYwaDIyOS40ODlsNTQuNjI3LTU0LjYyN2MxMi40OTgtMTIuNDk3IDMyLjc1OC0xMi40OTcgNDUuMjU2IDAgMTIuNDk2IDEyLjQ5NyAxMi40OTYgMzIuNzU4IDAgNDUuMjU1TDQyNCAxOTcuMjU1VjI1Nmg1NmMxNy45NzIgMCAzMi40ODQgMTQuODE2IDMxLjk4OCAzMi45ek0yNTcgMGMtNjEuODU2IDAtMTEyIDUwLjE0NC0xMTIgMTEyaDIyNEMzNjkgNTAuMTQ0IDMxOC44NTYgMCAyNTcgMHoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=
)][Download] [![Discord](https://img.shields.io/discord/313125603924639766.svg?color=%237289da&label=Discord&logo=discord&logoColor=%237289da)][Discord] [![Support](https://img.shields.io/badge/Patreon-Support-orange.svg?logo=Patreon)][Patreon]
Forge is a free, open-source modding API all of your favourite mods use!
| Version | Support |
|---------| ------------- |
| 1.19.x | Active |
| 1.18.x | LTS |
| Version | Support |
|--------------|-----------------------------|
| All versions | [Supported][Support-Policy] |
* [Download]
* [Forum]
@@ -20,7 +19,7 @@ Forge is a free, open-source modding API all of your favourite mods use!
# Installing Forge
Go to [the Forge website](https://files.minecraftforge.net)
Go to [the Forge website][Download]
and select the Minecraft version you wish to get Forge for from the list.
You can download the installer for the *Recommended Build* or the
@@ -29,38 +28,68 @@ You can download the installer for the *Recommended Build* or the
into your vanilla launcher environment, where you can then create a new
profile using that version and play the game!
For support and questions, visit [the Support Forum](https://www.minecraftforge.net/forum/forum/18-support-bug-reports/) or [the Forge Discord server](https://discord.gg/forge).
For support and questions, visit [the Support Forum][Support-Forum] or [the Forge Discord server][Discord].
[Here is a short video from Rorax showing how to install and setup Forge.](https://www.youtube.com/watch?v=lB3ArN_-3Oc)
## Required Java Versions
We require the version of Java that Minecraft requires, which is currently:
| Minecraft | Java |
|---------------|-------------------|
| 1.20.6+ | [21][Adoptium-21] |
| 1.18 - 1.20.4 | [17][Adoptium-17] |
| 1.17.1 | [16][Adoptium-16] |
| 1.16.5- | [8][Adoptium-8] |
**If you are on Windows and have difficulty using Java to launch JAR files, consider using [Jarfix by Johann Löfflmann][Jarfix].**
# Creating Mods
[See the "Getting Started" section in the Forge Documentation](https://mcforge.readthedocs.io/en/latest/gettingstarted/).
[See the "Getting Started" section in the Forge Documentation][Getting-Started].
# Contribute to Forge
If you wish to actually inspect Forge, submit PRs or otherwise work
with Forge itself, you're in the right place!
[See the guide to setting up a Forge workspace](http://mcforge.readthedocs.io/en/latest/forgedev/).
[See the guide to setting up a Forge workspace][ForgeDev].
### Pull requests
[See the "Making Changes and Pull Requests" section in the Forge documentation](https://mcforge.readthedocs.io/en/latest/forgedev/#making-changes-and-pull-requests).
[See the "Making Changes and Pull Requests" section in the Forge documentation][Pull-Requests].
Please read the contributing guidelines found [here](CONTRIBUTING.md) before making a pull request.
Please read the contributing guidelines found [here][Contributing] before making a pull request.
### Contributor License Agreement
We require all contributors to acknowledge the [Forge Contributor License Agreement](https://cla-assistant.io/MinecraftForge/MinecraftForge).
We require all contributors to acknowledge the [Forge Contributor License Agreement][CLA].
Please ensure you have a valid email address associated with your GitHub account to do this. If you have previously
signed it, you should be OK.
#### Donate
*Forge is a large project with many collaborators working on it around the clock. Forge is and will always remain free
to use and modify. However, it costs money to run such a large project as this, so please consider
[becoming a patron](https://www.patreon.com/LexManos).*
[becoming a patron][Patreon].*
[Contributing]: ./CONTRIBUTING.md
[CLA]: https://cla-assistant.io/MinecraftForge/MinecraftForge
[Download]: https://files.minecraftforge.net/
[Forum]: https://www.minecraftforge.net/forum/
[Discord]: https://discord.gg/UvedJ9m
[Documentation]: https://mcforge.readthedocs.io
[Adoptium-21]: https://adoptium.net/temurin/releases/?version=21
[Adoptium-17]: https://adoptium.net/temurin/releases/?version=17
[Adoptium-16]: https://adoptium.net/temurin/releases/?version=16
[Adoptium-8]: https://adoptium.net/temurin/releases/?version=8
[Jarfix]: https://johann.loefflmann.net/en/software/jarfix/index.html
[Forum]: https://forums.minecraftforge.net/
[Support-Forum]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
[Support-Policy]: https://forums.minecraftforge.net/topic/144690-new-tiered-support-policy
[Discord]: https://discord.minecraftforge.net/
[Documentation]: https://docs.minecraftforge.net/
[Getting-Started]: https://docs.minecraftforge.net/en/latest/gettingstarted/
[ForgeDev]: https://docs.minecraftforge.net/en/latest/forgedev/
[Pull-Requests]: https://docs.minecraftforge.net/en/latest/forgedev/#making-changes-and-pull-requests
[Patreon]: https://www.patreon.com/LexManos
+15 -8
View File
@@ -1,7 +1,7 @@
Getting Support Surrounding Forge
=================================
**If you do not want to read the following wall of text, no matter its relevance for helping get your issue resolved quickly, please refer to the [Forge Forums](http://www.minecraftforge.net/forum/forum/18-support-bug-reports/) for support.**
**If you do not want to read the following wall of text, no matter its relevance for helping get your issue resolved quickly, please refer to the [Forge Forums][Bug-Reports] for support.**
Before reporting an issue on this repository's bug tracker, please carefully read this document. There are various resources that are more likely to be able to help you with certain kinds of issues. This document provides instructions on how to determine which place best to report to.
@@ -44,7 +44,7 @@ You can generally tell pretty quickly what caused the error at the head of your
1. If you can find the name of a mod in the trace, that mod most likely is the culprit.
- Be aware that you may find multiple mods as participants of a crash. In that case, you can either report to both or try to reason (using the names in the stacktrace) about which mod is at fault.
2. If you can only find Forge (usually starting with `net.minecraftforge`) alongside Vanilla (usually starting with `net.minecraft`) references, chances are that you have found a bug in Forge. In this case, use one of the various [support channels](#forge-support) listed below.
- The occurence of a single call to Forge somewhere in the stacktrace *does not* warrant a report to Forge if there are mods present in the crash report.
- The occurrence of a single call to Forge somewhere in the stacktrace *does not* warrant a report to Forge if there are mods present in the crash report.
3. If there's no mention of Forge or a mod, but any Vanilla class, you might have found a Vanilla bug. You should probably still report it via the Forge channels first, however, due the nature of modded environments and the rarity of Vanilla crash bugs.
4. Always check whether there's a mention of coremods at the beginning of a crash report. If there is and you can't find any clear culprit, consider reporting to these coremods first, since they are free to change any of the code executed in your environment.
@@ -74,10 +74,10 @@ Please note that this list has to be considered incomplete as there is a large n
If you're certain you've found an issue that is directly caused by Forge, you have the following three platforms available for dealing with it, with decreasing relevance for end-users:
1. **Forge Forums:** There's a dedicated [Support & Bug Report](http://www.minecraftforge.net/forum/forum/18-support-bug-reports/) section on the Forge forums, which is led by team of moderators and volunteers who will try to deal with your reports as quickly as possible. *As a player you should probably report here first.*
- For modders, there also is a separate [Modder Support](http://www.minecraftforge.net/forum/forum/70-modder-support/) section where you can ask all your development-related questions. *You should prefer this section for coding help.*
2. **Forge Discord Server:** You can find a lot of help on the official [Discord Server](https://discord.gg/forge), especially smaller issues that you quickly need help with. *Be aware that the channel is more suited towards development questions, so while you may get support there as a player, you should almost always prefer the forums.*
3. **Forge Issue Tracker:** If you're absolutely certain that you've found an issue in the Forge codebase (e.g. you're a modder yourself and have analyzed the stacktrace and relevant code), don't refrain from reporting it on this repository's [issue tracker](https://github.com/MinecraftForge/MinecraftForge/issues). *Please do not use the issues as a means of getting coding help. For developers, the IRC and the [official documentation](https://mcforge.readthedocs.io) are the best sources of information.*
1. **Forge Forums:** There's a dedicated [Support & Bug Report][Bug-Reports] section on the Forge forums, which is led by team of moderators and volunteers who will try to deal with your reports as quickly as possible. *As a player you should probably report here first.*
- For modders, there also is a separate [Modder Support][Modder-Support] section where you can ask all your development-related questions. *You should prefer this section for coding help.*
2. **Forge Discord Server:** You can find a lot of help on the official [Discord Server][Discord], especially smaller issues that you quickly need help with. *Be aware that the channel is more suited towards development questions, so while you may get support there as a player, you should almost always prefer the forums.*
3. **Forge Issue Tracker:** If you're absolutely certain that you've found an issue in the Forge codebase (e.g. you're a modder yourself and have analyzed the stacktrace and relevant code), don't refrain from reporting it on this repository's [issue tracker](https://github.com/MinecraftForge/MinecraftForge/issues). *Please do not use the issues as a means of getting coding help. For developers, the IRC and the [official documentation][Documentation] are the best sources of information.*
### Mod Support
@@ -99,7 +99,7 @@ Now that you've identified the source of your issue and decided on the correct s
### Preventing Duplicates and Unnecessary Reports
Use the target issue tracker's search functionality to look for issues which have descriptions similar to yours, but make sure to also search for already closed issues. If you can find one, verify it is in fact the same (or a very similar issue) and simply comment on it if you have anything else to contribute to the discussion, such as other steps to reproduce it. *Do not create duplicates of an existing issue, since it only puts extra effort on maintainers to recognize and close/mark them.*
Additionally, always check the *latest* changelogs of Forge and participating mods to find out if the bug was already noticed and fixed by the developer. Often the maintainer may only support newer versions of the game, so also check the changelogs for versions of Minecraft you don't actually actively play. If the issue was adressed already, *do not report it*, even if the fix only exists for a newer version of the game, since the developer probably had a good reason to do so. If your issue only exists with versions of a mod that are for very old versions of the game, also refrain from reporting them, considering most modders only support the two latest versions of Minecraft, if at all.
Additionally, always check the *latest* changelogs of Forge and participating mods to find out if the bug was already noticed and fixed by the developer. Often the maintainer may only support newer versions of the game, so also check the changelogs for versions of Minecraft you don't actually actively play. If the issue was addressed already, *do not report it*, even if the fix only exists for a newer version of the game, since the developer probably had a good reason to do so. If your issue only exists with versions of a mod that are for very old versions of the game, also refrain from reporting them, considering most modders only support the two latest versions of Minecraft, if at all.
### Required Information
If you're certain that your issue went unnoticed up until now, you can get to actually writing up the report. Below you can find a list of the *required* information any bug report should contain:
@@ -144,4 +144,11 @@ After you have written your bug report, it is time to *wait*. Please be *patient
Every project has a different way of dealing with issues, so don't wonder if there's no comment on it, but somebody "labels" it with certain markers. This helps developers to more easily keep track of bugs and priorities. When any further input from you is required, you will be asked specifically. Do not bug anyone through other channels about resolving your issue.
Once your issue is closed, you will know whether it was valid or turned out to be irreproducible. If you included all of the data from above in your report, chances are that the latter won't be the case.
Once your issue is closed, you will know whether it was valid or turned out to be unreproducible. If you included all of the data from above in your report, chances are that the latter won't be the case.
[Bug-Reports]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
[Modder-Support]: https://forums.minecraftforge.net/forum/70-modder-support/
[Discord]: https://discord.minecraftforge.net/
[Documentation]: https://docs.minecraftforge.net/
+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
+44 -50
View File
@@ -1,73 +1,67 @@
plugins {
id 'com.github.ben-manes.versions'
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
id 'org.cadixdev.licenser'
}
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply plugin: 'org.javamodularity.moduleplugin'
import net.minecraftforge.gradleutils.PomUtils
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
apply from: rootProject.file('build_shared.gradle')
dependencies {
compileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
compileOnly(libs.nulls)
api("net.minecraftforge:eventbus:${EVENTBUS_VERSION}")
api(libs.eventbus)
implementation(project(':fmlloader'))
implementation("commons-io:commons-io:${COMMONS_IO_VERSION}")
implementation(libs.commons.io)
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
ext {
MANIFESTS = [
'': [
'FMLModType': 'LIBRARY',
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
'Git-Commit': GIT_INFO.abbreviatedId,
'Git-Branch': GIT_INFO.branch,
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}"
] as LinkedHashMap,
'net/minecraftforge/fml/loading/': [
'Specification-Title': 'FMLLoader',
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 Loader',
'Implementation-Title': 'FML',
'Implementation-Version': '1.0',
'Implementation-Vendor': 'Forge'
] as LinkedHashMap
]
}
jar.doFirst {
MANIFESTS.each { pkg, values ->
if (pkg == '')
manifest.attributes(values)
else
manifest.attributes(values, pkg)
'Implementation-Vendor': 'Forge Development LLC'
] as LinkedHashMap, 'net/minecraftforge/fml/')
}
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:-unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
include 'net/minecraftforge/'
}
artifacts {
archives jar
archives sourcesJar
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
}
}
publishing.publications.mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
}
@@ -5,36 +5,39 @@
package net.minecraftforge.fml;
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.config.IConfigEvent;
import net.minecraftforge.fml.loading.FMLLoader;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.function.Supplier;
/**
* Used to allow access to certain things from the game layer
*/
public class Bindings {
private static final Bindings INSTANCE = new Bindings();
private Bindings() {}
private final IBindingsProvider provider;
private Bindings() {
final var providers = ServiceLoaderUtils.streamServiceLoader(()->ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class), sce->{}).toList();
if (providers.size() != 1) {
throw new IllegalStateException("Could not find bindings provider");
}
this.provider = providers.get(0);
}
/**
* The provider of the bindings
* @implNote May also throw a {@link ServiceConfigurationError}
*/
private static final IBindingsProvider PROVIDER = ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class)
.findFirst().orElseThrow(() -> new IllegalStateException("Could not find bindings provider"));
/**
* @return A supplier of net.minecraftforge.common.MinecraftForge#EVENT_BUS
*/
public static Supplier<IEventBus> getForgeBus() {
return INSTANCE.provider.getForgeBusSupplier();
return PROVIDER.getForgeBusSupplier();
}
public static Supplier<I18NParser> getMessageParser() {
return INSTANCE.provider.getMessageParser();
return PROVIDER.getMessageParser();
}
public static Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration() {
return INSTANCE.provider.getConfigConfiguration();
return PROVIDER.getConfigConfiguration();
}
}
@@ -5,33 +5,93 @@
package net.minecraftforge.fml;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
public class CrashReportCallables {
public class CrashReportCallables
{
private static final Logger LOGGER = LogUtils.getLogger();
private static final List<ISystemReportExtender> crashCallables = Collections.synchronizedList(new ArrayList<>());
/**
* Register a custom {@link ISystemReportExtender}
*/
public static void registerCrashCallable(ISystemReportExtender callable)
{
crashCallables.add(callable);
}
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator) {
registerCrashCallable(new ISystemReportExtender() {
/**
* Register a {@link ISystemReportExtender system report extender} with the given header name and content
* generator, which will always be appended to the system report
* @param headerName The name of the system report entry
* @param reportGenerator The report generator to be called when a crash report is built
*/
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator)
{
registerCrashCallable(new ISystemReportExtender()
{
@Override
public String getLabel() {
public String getLabel()
{
return headerName;
}
@Override
public String get() {
public String get()
{
return reportGenerator.get();
}
});
}
public static List<ISystemReportExtender> allCrashCallables() {
/**
* Register a {@link ISystemReportExtender system report extender} with the given header name and content
* generator, which will only be appended to the system report when the given {@link BooleanSupplier} returns true
* @param headerName The name of the system report entry
* @param reportGenerator The report generator to be called when a crash report is built
* @param active The supplier of the flag to be checked when a crash report is built
*/
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator, BooleanSupplier active)
{
registerCrashCallable(new ISystemReportExtender()
{
@Override
public String getLabel()
{
return headerName;
}
@Override
public String get()
{
return reportGenerator.get();
}
@Override
public boolean isActive()
{
try
{
return active.getAsBoolean();
}
catch (Throwable t)
{
LOGGER.warn("CrashCallable '{}' threw an exception while checking the active flag, disabling", headerName, t);
return false;
}
}
});
}
public static List<ISystemReportExtender> allCrashCallables()
{
return List.copyOf(crashCallables);
}
}
@@ -11,9 +11,11 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import com.google.common.base.Stopwatch;
@@ -53,17 +55,31 @@ public class DeferredWorkQueue
public void runTasks() {
if (tasks.isEmpty()) return;
LOGGER.debug(LOADING, "Dispatching synchronous work for work queue {}: {} jobs", modLoadingStage, tasks.size());
RuntimeException aggregate = new RuntimeException();
Stopwatch timer = Stopwatch.createStarted();
tasks.forEach(t->makeRunnable(t, Runnable::run));
for (TaskInfo t : tasks) {
makeRunnable(t, Runnable::run, aggregate);
}
timer.stop();
LOGGER.debug(LOADING, "Synchronous work queue completed in {}", timer);
if (aggregate.getSuppressed().length > 0) {
LOGGER.fatal(
LOADING,
"Synchronous work queue completed exceptionally in {}, see suppressed exceptions for details:",
timer,
aggregate
);
throw aggregate;
} else {
LOGGER.debug(LOADING, "Synchronous work queue completed in {}", timer);
}
}
private static void makeRunnable(TaskInfo ti, Executor executor) {
private static void makeRunnable(TaskInfo ti, Executor executor, RuntimeException aggregate) {
executor.execute(() -> {
Stopwatch timer = Stopwatch.createStarted();
ModLoadingContext.get().setActiveContainer(ti.owner);
try {
ti.future.exceptionally(t -> captureException(ti.owner.getModId(), aggregate, t));
ti.task.run();
} finally {
ModLoadingContext.get().setActiveContainer(null);
@@ -75,13 +91,38 @@ public class DeferredWorkQueue
});
}
private static <T> T captureException(String modId, RuntimeException aggregate, Throwable throwable) {
if (throwable instanceof CompletionException ce) {
throwable = ce.getCause();
}
aggregate.addSuppressed(throwable);
LOGGER.error("Mod '{}' encountered an error in a deferred task:", modId, throwable);
return null;
}
public CompletableFuture<Void> enqueueWork(final ModContainer modInfo, final Runnable work) {
return CompletableFuture.runAsync(work, r->tasks.add(new TaskInfo(modInfo, r)));
return enqueueWork(modInfo, taskInfo -> CompletableFuture.runAsync(work, r -> taskInfo.task = r));
}
public <T> CompletableFuture<T> enqueueWork(final ModContainer modInfo, final Supplier<T> work) {
return CompletableFuture.supplyAsync(work, r->tasks.add(new TaskInfo(modInfo, r)));
return enqueueWork(modInfo, taskInfo -> CompletableFuture.supplyAsync(work, r -> taskInfo.task = r));
}
record TaskInfo(ModContainer owner, Runnable task) {}
private <T> CompletableFuture<T> enqueueWork(final ModContainer modInfo, Function<TaskInfo, CompletableFuture<T>> futureGen) {
TaskInfo taskInfo = new TaskInfo(modInfo);
CompletableFuture<T> future = futureGen.apply(taskInfo);
taskInfo.future = future;
tasks.add(taskInfo);
return future;
}
private static final class TaskInfo {
private final ModContainer owner;
private Runnable task;
private CompletableFuture<?> future;
private TaskInfo(ModContainer owner) {
this.owner = owner;
}
}
}
@@ -25,6 +25,7 @@ import java.util.function.Supplier;
* @param <T> the type of the record which is held by the extension point
*/
@SuppressWarnings("unused") // Type parameter T
// TODO: Complete re-write, this hasnt actually be used in a generic way by mods and we could make it a lot simpler.
public interface IExtensionPoint<T extends Record>
{
/**
@@ -40,7 +41,7 @@ public interface IExtensionPoint<T extends Record>
* where {@code true} means it is from the server and {@code false} means it is from the world save. The return
* value of the predicate determines whether the remote version is "compatible" for the purposes of the display test.</p>
*
* <p>The local compatibility version may be of the value {@link net.minecraftforge.network.NetworkConstants#IGNORESERVERONLY},
* <p>The local compatibility version may be of the value {@link DisplayTest#IGNORESERVERONLY},
* in which case clients will ignore the mod's presence if it is present on the server but not on the client.
* However, the remote version test predicate must still accept this value as a remote version in order to display
* as compatible if the mod is present on the client.</p>
@@ -94,6 +95,27 @@ public interface IExtensionPoint<T extends Record>
*/
@SuppressWarnings("JavadocReference") // reference to NetworkConstants, ForgeHooksClient
record DisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) implements IExtensionPoint<DisplayTest> {
public static final String IGNORESERVERONLY = "OHNOES\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31";
public static final String IGNORESERVERONLY = "SERVER_ONLY";
/**
* Ignores any version information coming from the server - use for server only mods
*/
public static final Supplier<DisplayTest> IGNORE_SERVER_VERSION = () -> new DisplayTest(IGNORESERVERONLY, (remoteVersion, isFromServer) -> true);
/**
* Ignores all information and provides no information
* <p>Note: If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or
* Open to LAN), consider setting {@code clientSideOnly=true} in the root of your mods.toml.</p>
*/
public static final Supplier<DisplayTest> IGNORE_ALL_VERSION = () -> new DisplayTest("", (remoteVersion, isFromServer) -> true);
/**
* An optional alternative to {@link #DisplayTest(Supplier, BiPredicate)} which accepts a constant version string
* instead of a {@link Supplier}.
* <p>Internally, the provided version string is wrapped in a Supplier for you.</p>
*/
public DisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
this(() -> version, remoteVersionTest);
}
}
}
@@ -7,12 +7,14 @@ 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.ToIntFunction;
/**
* A mod loading state. During mod loading, the mod loader transitions between states in a defined sorted list of states,
@@ -44,43 +46,55 @@ public interface IModLoadingState {
*/
Function<ModList, String> message();
/**
* @return a function that computes the size of this transition based on the size of the modlist.
* Used to compute progress.
*/
ToIntFunction<ModList> size();
/**
* {@return an optional runnable, which runs before starting the transition from this state to the next}
* @see #buildTransition(Executor, Executor, Function, Function)
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
*/
Optional<Consumer<ModList>> inlineRunnable();
/**
* Builds the transition task for this state with a blank pre-sync and post-sync task.
*
* @param syncExecutor a synchronous executor
* @param <T> a type of event fired on the mod-specific event bus
* @param syncExecutor a synchronous executor
* @param parallelExecutor a parallel executor
* @param <T> a type of event fired on the mod-specific event bus
* @param progressBar a progress meter for tracking progress
* @return a transition task for this state
* @see #buildTransition(Executor, Executor, Function, Function)
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
*/
default <T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor) {
return buildTransition(syncExecutor, parallelExecutor,
e -> CompletableFuture.runAsync(() -> {}, e),
e -> CompletableFuture.runAsync(() -> {}, e));
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)
);
}
/**
* Builds the transition task for this state. The pre-sync and post-sync task functions allow the transition builder
* to run these tasks on the same executor as the actual event dispatch and pre/post hooks.
*
* @param syncExecutor a synchronous executor
* @param <T> a type of event fired on the mod-specific event bus
* @param syncExecutor a synchronous executor
* @param parallelExecutor a parallel executor
* @param preSyncTask a function which returns a task to run before event pre-dispatch hook
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
* @param <T> a type of event fired on the mod-specific event bus
* @param progressBar a progress meter for tracking progress
* @param preSyncTask a function which returns a task to run before event pre-dispatch hook
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
* @return a transition task for this state
*/
<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);
}
@@ -7,9 +7,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.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
@@ -17,96 +16,67 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@SuppressWarnings("unchecked")
import org.jetbrains.annotations.Nullable;
public interface IModStateTransition {
/** Magic value to allow me to optimize the futures list by ignoring the default value without making old methods nullable. */
public static final BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> NULL_HOOK = (e, g) -> CompletableFuture.completedFuture(null);
static IModStateTransition buildNoopTransition() {
return new NoopTransition();
return ModStateTransitionHelper.NOOP;
}
default <T extends Event & IModBusEvent>
CompletableFuture<Void> build(final Executor syncExecutor,
final Executor parallelExecutor,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
List<CompletableFuture<Void>> futures = new ArrayList<>();
this.eventFunctionStream().get()
.map(f -> (EventGenerator<T>) f)
.reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, head, ModLoadingStage::currentState, tail))
.ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, last, nextModLoadingStage(), null));
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
final CompletableFuture<Void> eventDispatchCF = ModList.gather(futures).thenCompose(ModList::completableFutureFromExceptionList);
final CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
.thenComposeAsync(n -> eventDispatchCF, parallelExecutor)
.thenApply(r -> {
postSyncTask.apply(syncExecutor);
return null;
});
return this.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
default CompletableFuture<Void> build(
final String name,
final Executor syncExecutor,
final Executor parallelExecutor,
final ProgressMeter progressBar,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask
) {
return ModStateTransitionHelper.build(this, name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
}
default BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextModLoadingStage() {
return ModLoadingStage::nextState;
}
private <T extends Event & IModBusEvent>
EventGenerator<T> addCompletableFutureTaskForModDispatch(final Executor syncExecutor,
final Executor parallelExecutor,
final List<CompletableFuture<Void>> completableFutures,
final EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState,
final EventGenerator<T> nextGenerator) {
final Executor selectedExecutor = threadSelector().apply(syncExecutor, parallelExecutor);
var preDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) preDispatchHook();
completableFutures.add(preDispatchHook.apply(selectedExecutor, eventGenerator));
completableFutures.add(ModList.get().futureVisitor(eventGenerator, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor)));
var postDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) postDispatchHook();
completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator));
return nextGenerator;
/**
* 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 Event & IModBusEvent> EventGenerator<T> eventFunction() {
return null;
}
Supplier<Stream<EventGenerator<?>>> eventFunctionStream();
ThreadSelector threadSelector();
BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator();
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook();
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook();
/**
* 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 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);
}
}
@@ -10,4 +10,9 @@ import java.util.function.Supplier;
public interface ISystemReportExtender extends Supplier<String>
{
String getLabel();
default boolean isActive()
{
return true;
}
}
@@ -61,7 +61,7 @@ public class InterModComms
}
}
private static ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
/**
* Send IMC to remote. Sender will default to the active modcontainer, or minecraft if not.
@@ -115,16 +115,14 @@ public class InterModComms
return getMessages(modId, s->Boolean.TRUE);
}
private static class QueueFilteringSpliterator implements Spliterator<IMCMessage>
{
private final ConcurrentLinkedQueue<IMCMessage> queue;
private final Predicate<String> methodFilter;
private final Iterator<IMCMessage> iterator;
private record QueueFilteringSpliterator(
ConcurrentLinkedQueue<IMCMessage> queue,
Predicate<String> methodFilter,
Iterator<IMCMessage> iterator
) implements Spliterator<IMCMessage> {
public QueueFilteringSpliterator(final ConcurrentLinkedQueue<IMCMessage> queue, final Predicate<String> methodFilter) {
this.queue = queue;
this.iterator = queue.iterator();
this.methodFilter = methodFilter;
this(queue, methodFilter, queue.iterator());
}
@Override
@@ -16,8 +16,7 @@ import net.minecraftforge.api.distmarker.Dist;
*
* @see Dist
*/
public enum LogicalSide
{
public enum LogicalSide {
/**
* The logical client of the Minecraft game, which interfaces with the player's inputs and renders the player's
* viewpoint.
@@ -27,6 +26,7 @@ public enum LogicalSide
* @see Dist#CLIENT
*/
CLIENT,
/**
* The logical server of the Minecraft game, responsible for connecting to clients and running the simulation logic
* on the level.
@@ -41,16 +41,14 @@ public enum LogicalSide
/**
* {@return if this logical side is the server}
*/
public boolean isServer()
{
public boolean isServer() {
return !isClient();
}
/**
* {@return if the logical side is the client}
*/
public boolean isClient()
{
public boolean isClient() {
return this == CLIENT;
}
}
@@ -12,14 +12,13 @@ import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.forgespi.language.IModInfo;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -31,41 +30,40 @@ import java.util.function.Supplier;
* a mechanism by which we can wrap actual mod code so that the loader and other
* facilities can treat mods at arms length.
* </p>
*
* @author cpw
*
*/
public abstract class ModContainer
{
public abstract class ModContainer {
protected final String modId;
protected final String namespace;
protected final IModInfo modInfo;
protected ModLoadingStage modLoadingStage;
protected Supplier<?> contextExtension;
protected final Map<ModLoadingStage, Runnable> activityMap = new HashMap<>();
protected final Map<ModLoadingStage, Runnable> activityMap = new EnumMap<>(ModLoadingStage.class);
protected final Map<Class<? extends IExtensionPoint<?>>, Supplier<?>> extensionPoints = new IdentityHashMap<>();
protected final EnumMap<ModConfig.Type, ModConfig> configs = new EnumMap<>(ModConfig.Type.class);
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final Set<ModContainer> dependencies = new HashSet<>();
/**
* If you want to handle the event, override {@link #dispatchConfigEvent(IConfigEvent)}
*/
@Deprecated(since = "1.21.3", forRemoval = true)
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
public ModContainer(IModInfo info)
{
public ModContainer(IModInfo info) {
this.modId = info.getModId();
// TODO: Currently not reading namespace from configuration..
this.namespace = this.modId;
this.modInfo = info;
this.modLoadingStage = ModLoadingStage.CONSTRUCT;
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest").orElse("MATCH_VERSION"); // missing defaults to DEFAULT type
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest")
.orElseGet(() -> info.getOwningFile().isClientSideOnly() ? "IGNORE_ALL_VERSION" : "MATCH_VERSION");
Supplier<IExtensionPoint.DisplayTest> displayTestSupplier = switch (displayTestString) {
case "MATCH_VERSION" -> // default displaytest checks for version string match
() -> new IExtensionPoint.DisplayTest(() -> this.modInfo.getVersion().toString(),
(incoming, isNetwork) -> Objects.equals(incoming, this.modInfo.getVersion().toString()));
case "IGNORE_SERVER_VERSION" -> // Ignores any version information coming from the server - use for server only mods
() -> new IExtensionPoint.DisplayTest(() -> IExtensionPoint.DisplayTest.IGNORESERVERONLY, (incoming, isNetwork) -> true);
IExtensionPoint.DisplayTest.IGNORE_SERVER_VERSION;
case "IGNORE_ALL_VERSION" -> // Ignores all information and provides no information
() -> new IExtensionPoint.DisplayTest(() -> "", (incoming, isNetwork) -> true);
IExtensionPoint.DisplayTest.IGNORE_ALL_VERSION;
case "NONE" -> null; // NO display test at all - use this if you're going to do your own display test
default -> // any other value throws an exception
throw new IllegalArgumentException("Invalid displayTest value supplied in mods.toml");
@@ -79,56 +77,35 @@ public abstract class ModContainer
/**
* Errored container state, used for filtering. Does nothing.
*/
ModContainer()
{
ModContainer() {
this.modLoadingStage = ModLoadingStage.ERROR;
modId = "BROKEN";
namespace = "BROKEN";
modInfo = null;
}
/**
* @return the modid for this mod
*/
public final String getModId()
{
public final String getModId() {
return modId;
}
/**
* @return the resource prefix for the mod
*/
public final String getNamespace()
{
public final String getNamespace() {
return namespace;
}
/**
* @return The current loading stage for this mod
*/
public ModLoadingStage getCurrentState()
{
public ModLoadingStage getCurrentState() {
return modLoadingStage;
}
public static <T extends Event & IModBusEvent> CompletableFuture<Void> buildTransitionHandler(
final ModContainer target,
final IModStateTransition.EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChangeHandler,
final Executor executor) {
return CompletableFuture
.runAsync(() -> {
ModLoadingContext.get().setActiveContainer(target);
target.activityMap.getOrDefault(target.modLoadingStage, ()->{}).run();
target.acceptEvent(eventGenerator.apply(target));
}, executor)
.whenComplete((mc, exception) -> {
target.modLoadingStage = stateChangeHandler.apply(target.modLoadingStage, exception);
ModLoadingContext.get().setActiveContainer(null);
});
}
public IModInfo getModInfo()
{
public IModInfo getModInfo() {
return modInfo;
}
@@ -137,17 +114,35 @@ public abstract class ModContainer
return Optional.ofNullable((T)extensionPoints.getOrDefault(point,()-> null).get());
}
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension)
{
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
extensionPoints.put(point, extension);
}
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> displayTest);
}
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
registerExtensionPoint(IExtensionPoint.DisplayTest.class, displayTest);
}
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
}
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
}
public void addConfig(final ModConfig modConfig) {
configs.put(modConfig.getType(), modConfig);
}
public void dispatchConfigEvent(IConfigEvent event) {
configHandler.ifPresent(configHandler->configHandler.accept(event));
var handler = configHandler.orElse(null);
if (handler != null) {
handler.accept(event);
}
}
/**
@@ -5,8 +5,6 @@
package net.minecraftforge.fml;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.forgespi.language.IModFileInfo;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.ModFileScanData;
@@ -14,27 +12,15 @@ import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.forgespi.locating.IModFile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
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;
@@ -44,27 +30,25 @@ import java.util.stream.Stream;
* Master list of all mods - game-side version. This is classloaded in the game scope and
* can dispatch game level events as a result.
*/
public class ModList
{
private static Logger LOGGER = LogManager.getLogger();
public class ModList {
private static ModList INSTANCE;
private final List<IModFileInfo> modFiles;
private final List<IModInfo> sortedList;
private final Map<String, ModFileInfo> fileById;
private final Map<String, IModFileInfo> 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)
{
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));
private ModList(final List<ModFile> modFiles, final List<ModInfo> sortedList) {
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 : this.modFiles) {
for (var mod : file.getMods())
byId.put(mod.getModId(), mod.getOwningFile());
}
this.fileById = Collections.unmodifiableMap(byId);
CrashReportCallables.registerCrashCallable("Mod List", this::crashReport);
}
@@ -73,19 +57,20 @@ public class ModList
}
private String fileToLine(IModFile mf) {
var mainMod = mf.getModInfos().getFirst();
return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|%-10.10s|Manifest: %s", mf.getFileName(),
mf.getModInfos().get(0).getDisplayName(),
mf.getModInfos().get(0).getModId(),
mf.getModInfos().get(0).getVersion(),
getModContainerState(mf.getModInfos().get(0).getModId()),
mainMod.getDisplayName(),
mainMod.getModId(),
mainMod.getVersion(),
getModContainerState(mainMod.getModId()),
((ModFileInfo)mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE"));
}
private String crashReport() {
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
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;
}
@@ -94,108 +79,47 @@ public class ModList
return INSTANCE;
}
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
thread.setName("modloading-worker-" + thread.getPoolIndex());
// The default sets it to the SystemClassloader, so copy the current one.
thread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
return thread;
}
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);
}
<T extends Event & IModBusEvent> Function<Executor, CompletableFuture<Void>> futureVisitor(
final IModStateTransition.EventGenerator<T> eventGenerator,
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChange) {
return executor -> gather(
this.mods.stream()
.map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, stateChange, executor))
.collect(Collectors.toList()))
.thenComposeAsync(ModList::completableFutureFromExceptionList, executor);
}
static CompletionStage<Void> completableFutureFromExceptionList(List<? extends Map.Entry<?, Throwable>> t) {
if (t.stream().noneMatch(e->e.getValue()!=null)) {
return CompletableFuture.completedFuture(null);
} else {
final List<Throwable> throwables = t.stream().filter(e -> e.getValue() != null).map(Map.Entry::getValue).collect(Collectors.toList());
CompletableFuture<Void> cf = new CompletableFuture<>();
final RuntimeException accumulator = new RuntimeException();
cf.completeExceptionally(accumulator);
throwables.forEach(exception -> {
if (exception instanceof CompletionException) {
exception = exception.getCause();
}
if (exception.getSuppressed().length!=0) {
Arrays.stream(exception.getSuppressed()).forEach(accumulator::addSuppressed);
} else {
accumulator.addSuppressed(exception);
}
});
return cf;
}
}
static <V> CompletableFuture<List<Map.Entry<V, Throwable>>> gather(List<? extends CompletableFuture<? extends V>> futures) {
List<Map.Entry<V, Throwable>> list = new ArrayList<>(futures.size());
CompletableFuture<?>[] results = new CompletableFuture[futures.size()];
futures.forEach(future -> {
int i = list.size();
list.add(null);
results[i] = future.whenComplete((result, exception) -> list.set(i, new AbstractMap.SimpleImmutableEntry<>(result, exception)));
});
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
}
void setLoadedMods(final List<ModContainer> modContainers)
{
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).
@@ -208,8 +132,7 @@ public class ModList
}
public void forEachModFile(Consumer<IModFile> fileConsumer)
{
public void forEachModFile(Consumer<IModFile> fileConsumer) {
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
}
@@ -225,13 +148,11 @@ 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);
}
}
}
@@ -10,11 +10,13 @@ import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
import net.minecraftforge.fml.loading.LoadingModList;
import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier;
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.forgespi.language.IModLanguageProvider;
import net.minecraftforge.forgespi.locating.ForgeFeature;
@@ -30,7 +32,6 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import static net.minecraftforge.fml.Logging.CORE;
import static net.minecraftforge.fml.Logging.LOADING;
@@ -70,48 +71,53 @@ 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 static ModLoader INSTANCE;
private final LoadingModList loadingModList;
private final List<ModLoadingException> loadingExceptions;
private final Set<IModInfo> erroredModInfos;
private final List<ModLoadingWarning> loadingWarnings;
private final ModStateManager stateManager;
private boolean loadingStateValid;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<Consumer<String>> statusConsumer = StartupMessageManager.modLoaderConsumer();
private static boolean loadingStateValid;
private final Consumer<String> statusConsumer = StartupNotificationManager.modLoaderConsumer().orElse(msg -> {});
private final Set<IModLoadingState> completedStates = new HashSet<>();
private ModList modList;
private ModLoader()
{
INSTANCE = this;
private ModLoader() {
this.loadingModList = FMLLoader.getLoadingModList();
this.loadingExceptions = FMLLoader.getLoadingModList().getErrors().stream()
this.loadingExceptions = this.loadingModList.getErrors().stream()
.flatMap(ModLoadingException::fromEarlyException)
.collect(Collectors.toList());
this.loadingWarnings = FMLLoader.getLoadingModList().getBrokenFiles().stream()
.toList();
this.loadingWarnings = this.loadingModList.getBrokenFiles().stream()
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
.collect(Collectors.toList());
FMLLoader.getLoadingModList().getModFiles().stream()
if (this.loadingExceptions.isEmpty()) {
this.erroredModInfos = Collections.emptySet();
} else {
this.erroredModInfos = Collections.newSetFromMap(new IdentityHashMap<>());
this.erroredModInfos.addAll(this.loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
}
this.loadingModList.getModFiles().stream()
.filter(ModFileInfo::missingLicense)
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(thisModInfo -> this.loadingExceptions.stream().map(ModLoadingException::getModInfo).anyMatch(otherInfo -> otherInfo == thisModInfo))) //Ignore files where any other mod already encountered an error
.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()))
.forEach(this.loadingExceptions::add);
this.stateManager = new ModStateManager();
CrashReportCallables.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo);
CrashReportCallables.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName);
CrashReportCallables.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming);
CrashReportCallables.registerCrashCallable("ModLauncher services", this::computeModLauncherServiceList);
CrashReportCallables.registerCrashCallable("FML Language Providers", this::computeLanguageList);
CrashReportCallables.registerCrashCallable("ModLauncher services", ModLoader::computeModLauncherServiceList);
CrashReportCallables.registerCrashCallable("FML Language Providers", ModLoader::computeLanguageList);
}
private String computeLanguageList() {
return "\n"+FMLLoader.getLanguageLoadingProvider().applyForEach(lp->lp.name() +"@"+ lp.getClass().getPackage().getImplementationVersion()).collect(Collectors.joining("\n\t\t", "\t\t", ""));
private static String computeLanguageList() {
return "\n" + FMLLoader.getLanguageLoadingProvider()
.applyForEach(lp -> lp.name() + "@" + lp.getClass().getPackage().getImplementationVersion())
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
}
private String computeModLauncherServiceList() {
private static String computeModLauncherServiceList() {
final List<Map<String, String>> mods = FMLLoader.modLauncherModList();
return "\n"+mods.stream().map(mod->mod.getOrDefault("file","nofile")+
" "+mod.getOrDefault("name", "missing")+
@@ -120,9 +126,13 @@ public class ModLoader
collect(Collectors.joining("\n\t\t","\t\t",""));
}
public static ModLoader get()
{
return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE;
public static ModLoader get() {
return LazyInit.INSTANCE;
}
private static final class LazyInit {
private static final ModLoader INSTANCE = new ModLoader();
private LazyInit() {}
}
/**
@@ -132,128 +142,171 @@ public class ModLoader
* @param periodicTask Optional periodic task to perform on the main thread while other activities run
*/
public void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
ForgeFeature.registerFeature("java_version", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.SERVER, System.getProperty("java.version")));
ForgeFeature.registerFeature("javaVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version")));
ForgeFeature.registerFeature("openGLVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.CLIENT, ImmediateWindowHandler.getGLVersion()));
loadingStateValid = true;
statusConsumer.ifPresent(c->c.accept("Waiting for scan to complete"));
FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask);
statusConsumer.ifPresent(c->c.accept("Loading mods"));
final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList(),
loadingModList.getMods());
if (!this.loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0));
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.getFirst());
statusConsumer.accept("ERROR DURING MOD LOADING");
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(loadingExceptions);
}
statusConsumer.ifPresent(c->c.accept("Validating features"));
List<? extends ForgeFeature.Bound> failedBounds = loadingModList.getMods().stream()
.map(ModInfo::getForgeFeatures)
.flatMap(Collection::stream)
.flatMap(List::stream)
.filter(bound -> !ForgeFeature.testFeature(FMLEnvironment.dist, bound))
.toList();
if (!failedBounds.isEmpty()) {
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods");
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds);
statusConsumer.accept("ERROR DURING MOD LOADING");
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(failedBounds.stream()
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb.featureName(), fb.featureBound(), ForgeFeature.featureValue(fb)))
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)))
.toList());
}
statusConsumer.ifPresent(c->c.accept("Building Mod List"));
final List<ModContainer> modContainers = loadingModList.getModFiles().stream()
.map(ModFileInfo::getFile)
.map(this::buildMods)
.<ModContainer>mapMulti(Iterable::forEach)
.toList();
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);
}
if (!loadingExceptions.isEmpty()) {
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0));
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.getFirst());
statusConsumer.accept("ERROR DURING MOD LOADING");
modList.setLoadedMods(Collections.emptyList());
loadingStateValid = false;
throw new LoadingFailedException(loadingExceptions);
}
modList.setLoadedMods(modContainers);
// 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());
this.modList = modList;
statusConsumer.ifPresent(c->c.accept("Dispatching gathering events"));
stateManager.getStates(ModLoadingPhase.GATHER).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept("Gathering phase complete"));
var stateList = stateManager.getStates(ModLoadingPhase.GATHER);
var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(this.modList)).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
progress.complete();
}
public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
statusConsumer.ifPresent(c->c.accept("Performing load phase"));
stateManager.getStates(ModLoadingPhase.LOAD).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept("Mod setup complete"));
var stateList = stateManager.getStates(ModLoadingPhase.LOAD);
var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
progress.complete();
}
public void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
statusConsumer.ifPresent(c->c.accept("Performing completion phase"));
stateManager.getStates(ModLoadingPhase.COMPLETE).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())));
var stateList = stateManager.getStates(ModLoadingPhase.COMPLETE);
var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
for (IModLoadingState mls : stateList) {
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
}
statusConsumer.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size()));
progress.complete();
}
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) {
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) {
if (!isLoadingStateValid()) {
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
return;
}
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
state.buildTransition(syncExecutor, parallelExecutor).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
progressBar.label(progressBar.name() + " working");
syncExecutor.drive(ticker);
var inlineRunnable = state.inlineRunnable().orElse(null);
if (inlineRunnable != null) handleInlineTransition(inlineRunnable, state, syncExecutor, ticker);
var transition = state.buildTransition(syncExecutor, parallelExecutor, progressBar).orElse(null);
if (transition != null) waitForTransition(state, syncExecutor, ticker, transition);
completedStates.add(state);
}
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask) {
if (!isLoadingStateValid()) {
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
return;
}
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
state.buildTransition(syncExecutor, parallelExecutor, preSyncTask, postSyncTask).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
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);
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) {
loadingStateValid = false;
Throwable t = e.getCause();
final List<Throwable> notModLoading = Arrays.stream(t.getSuppressed())
.filter(obj -> !(obj instanceof ModLoadingException))
.collect(Collectors.toList());
if (!notModLoading.isEmpty()) {
boolean hasNotModLoadingEx = Arrays.stream(t.getSuppressed())
.anyMatch(obj -> !(obj instanceof ModLoadingException));
if (hasNotModLoadingEx) {
LOGGER.fatal("Encountered non-modloading exceptions!", e);
statusConsumer.accept("ERROR DURING MOD LOADING");
throw e;
}
final List<ModLoadingException> modLoadingExceptions = Arrays.stream(t.getSuppressed())
.filter(ModLoadingException.class::isInstance)
.map(ModLoadingException.class::cast)
.collect(Collectors.toList());
.toList();
LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size());
statusConsumer.accept("ERROR DURING MOD LOADING");
throw new LoadingFailedException(modLoadingExceptions);
}
}
private List<ModContainer> buildMods(final IModFile modFile)
{
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
private List<ModContainer> buildMods(final IModFile modFile) {
final Map<String, IModInfo> modInfoMap = new HashMap<>();
for (var mod : modFile.getModFileInfo().getMods())
modInfoMap.put(mod.getModId(), mod);
LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader());
final List<ModContainer> containers = modFile.getScanResult().getTargets()
.entrySet()
.stream()
.map(e -> buildModContainerFromTOML(modFile, modInfoMap, e))
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (containers.size() != modInfoMap.size()) {
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList());
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().collect(Collectors.toList());
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);
}
LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}",
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: {}",
modFile.getFilePath(),
containers.size(), containerIds,
modInfoMap.size(), modIds);
@@ -268,17 +321,20 @@ 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).collect(Collectors.toList());
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).toList();
}
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap,
final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader
) {
try {
final String modId = idToProviderEntry.getKey();
final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue();
IModInfo info = Optional.ofNullable(modInfoMap.get(modId)).
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
orElseThrow(()->new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId));
IModInfo info = modInfoMap.get(modId);
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
if (info == null)
throw new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId);
return languageLoader.loadMod(info, modFile.getScanResult(), FMLLoader.getGameLayer());
} catch (ModLoadingException mle) {
// exceptions are caught and added to the error list for later handling
@@ -293,7 +349,12 @@ public class ModLoader
* and don't want to cause extraneous crashes due to trying to do things that aren't possible in a "broken load"
*/
public static boolean isLoadingStateValid() {
return get().loadingStateValid;
return loadingStateValid;
}
public boolean hasCompletedState(final String stateName) {
IModLoadingState state = stateManager.findState(stateName);
return completedStates.contains(state);
}
public <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
@@ -311,6 +372,7 @@ public class ModLoader
}
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(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());
@@ -319,6 +381,15 @@ public class ModLoader
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
return e;
}
@SuppressWarnings("removal")
public <T extends Event & 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 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());
@@ -331,13 +402,11 @@ public class ModLoader
});
}
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);
}
@@ -8,38 +8,57 @@ package net.minecraftforge.fml;
import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.config.IConfigSpec;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.BiPredicate;
import java.util.function.Supplier;
public class ModLoadingContext
{
public class ModLoadingContext {
private static final Logger LOGGER = LogUtils.getLogger();
private static final ThreadLocal<ModLoadingContext> context = ThreadLocal.withInitial(ModLoadingContext::new);
private ModContainer activeContainer;
private Object languageExtension;
private ModLoadingStage stage;
/**
* @deprecated Use the context provided by your language loader in your mod's constructor
*/
@Deprecated(forRemoval = true, since="1.21.1")
public static ModLoadingContext get() {
return context.get();
}
private ModContainer activeContainer;
/**
* @deprecated Going to be moved to ForgeHooks for Internal use.
*/
@Deprecated(forRemoval = true, since="1.21.1")
public void setActiveContainer(final ModContainer container) {
this.activeContainer = container;
this.languageExtension = container == null ? null : container.contextExtension.get();
}
/**
* Going to be moved to ForgeHooks for Internal use.
* @deprecated Override/Use {@link ModLoadingContext#getContainer()}
*/
@Deprecated(forRemoval = true, since="1.21.1")
public ModContainer getActiveContainer() {
return activeContainer == null ? ModList.get().getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!")) : activeContainer;
}
/**
* @deprecated Going to be moved to ForgeHooks for Internal use.
*/
@Deprecated(forRemoval = true, since="1.21.1")
public String getActiveNamespace() {
return activeContainer == null ? "minecraft" : activeContainer.getNamespace();
}
/**
* @return {@link ModLoadingContext#getActiveContainer()} by default.
*/
public ModContainer getContainer() {
return getActiveContainer();
}
/**
* Register an {@link IExtensionPoint} with the mod container.
* @param point The extension point to register
@@ -47,29 +66,67 @@ public class ModLoadingContext
* @param <T> The type signature of the extension operator
*/
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
getActiveContainer().registerExtensionPoint(point, extension);
getContainer().registerExtensionPoint(point, extension);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)}.</p>
* @param displayTest The {@link IExtensionPoint.DisplayTest} to register
*/
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
getContainer().registerDisplayTest(() -> displayTest);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest supplier with {@link #registerExtensionPoint(Class, Supplier)}.</p>
* @param displayTest The {@link Supplier<IExtensionPoint.DisplayTest>} to register
*/
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
getContainer().registerDisplayTest(displayTest);
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
* creates the DisplayTest instance for you using the provided parameters.</p>
* @see IExtensionPoint.DisplayTest#DisplayTest(String, BiPredicate)
*/
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
}
/**
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
* creates the DisplayTest instance for you using the provided parameters.</p>
* @see IExtensionPoint.DisplayTest#DisplayTest(Supplier, BiPredicate)
*/
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
}
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec) {
if (spec.isEmpty())
{
// This handles the case where a mod tries to register a config, without any options configured inside it.
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getActiveContainer().getModId());
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getContainer().getModId());
return;
}
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer()));
getContainer().addConfig(new ModConfig(type, spec, getContainer()));
}
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec, String fileName) {
if (spec.isEmpty())
{
// This handles the case where a mod tries to register a config, without any options configured inside it.
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getActiveContainer().getModId(), fileName);
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getContainer().getModId(), fileName);
return;
}
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer(), fileName));
getContainer().addConfig(new ModConfig(type, spec, getContainer(), fileName));
}
@@ -7,12 +7,14 @@ 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.ToIntFunction;
/**
* Implementation of the {@link IModLoadingState} interface.
@@ -24,18 +26,27 @@ import java.util.function.Function;
* @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,
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 <T extends Event & IModBusEvent>
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
final Executor parallelExecutor,
final Function<Executor, CompletableFuture<Void>> preSyncTask,
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
return transition.map(t -> t.build(syncExecutor, parallelExecutor, preSyncTask, postSyncTask));
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()
: Optional.ofNullable(transition.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask));
}
/**
@@ -45,9 +56,12 @@ public record ModLoadingState(String name, String previous,
* @param name the name of the state
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) {
return new ModLoadingState(name, previous, ml -> "", phase, Optional.empty(), Optional.empty());
return of(name, phase).after(previous).empty();
}
/**
@@ -58,11 +72,13 @@ public record ModLoadingState(String name, String previous,
* @param previous the name of the immediately previous state to this state
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
* @return a mod loading state with state transition information and a default message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase,
final IModStateTransition transition) {
return new ModLoadingState(name, previous, ml -> "Processing transition " + name, phase, Optional.empty(), Optional.of(transition));
return of(name, phase).after(previous).withTransition(transition);
}
/**
@@ -74,11 +90,14 @@ public record ModLoadingState(String name, String previous,
* @param phase the mod loading phase the state belongs to
* @param transition the state transition information
* @return a mod loading state with state transition information and a custom message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withTransition(final String name, final String previous,
final Function<ModList, String> message, final ModLoadingPhase phase,
final IModStateTransition transition) {
return new ModLoadingState(name, previous, message, phase, Optional.empty(), Optional.of(transition));
return of(name, phase).after(previous).message(message).withTransition(transition);
}
/**
@@ -90,9 +109,59 @@ public record ModLoadingState(String name, String previous,
* @param phase the mod loading phase the state belongs to
* @param inline an optional runnable, which runs before starting the transition from this state to the next
* @return a mod loading state with an inline runnable and default message
*
* @deprecated Use the builder
*/
@Deprecated(since = "1.21.3", forRemoval = true)
public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase,
final Consumer<ModList> inline) {
return new ModLoadingState(name, previous, ml -> "Processing work " + name, phase, Optional.of(inline), Optional.empty());
return of(name, phase).after(previous).withInline(inline);
}
public static Builder of(final String name, final ModLoadingPhase phase) {
return new Builder(name, phase);
}
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());
}
}
}
@@ -12,8 +12,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class ModLoadingWarning
{
public class ModLoadingWarning {
/**
* Mod Info for mod with warning
*/
@@ -16,15 +16,13 @@ import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
public class ModStateManager {
static ModStateManager INSTANCE;
private final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
public ModStateManager() {
INSTANCE = this;
final var sp = ServiceLoader.load(FMLLoader.getGameLayer(), IModStateProvider.class);
this.stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, sce->{})
.map(IModStateProvider::getAllStates)
.<IModLoadingState>mapMulti(Iterable::forEach)
.flatMap(List::stream)
.collect(Collectors.groupingBy(IModLoadingState::phase, ()->new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
}
@@ -36,7 +34,18 @@ public class ModStateManager {
var dummy = ModLoadingState.empty("", "", phase);
nodes.forEach(graph::addNode);
graph.addNode(dummy);
nodes.forEach(n->graph.putEdge(lookup.getOrDefault(n.previous(), dummy), n));
for (IModLoadingState node : nodes) {
graph.putEdge(lookup.getOrDefault(node.previous(), dummy), node);
}
return TopologicalSort.topologicalSort(graph, Comparator.comparingInt(nodes::indexOf)).stream().filter(st->st!=dummy).toList();
}
public IModLoadingState findState(final String stateName) {
return stateMap.values()
.stream()
.flatMap(List::stream)
.filter(mls -> mls.name().equals(stateName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown IModLoadingState: " + stateName));
}
}
@@ -0,0 +1,181 @@
/*
* 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.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.eventbus.api.Event;
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(e -> e != null).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);
results[i] = future.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 Event & 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 Event & 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 Event & 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);
}
}
@@ -17,14 +17,16 @@ import static net.minecraftforge.fml.Logging.LOADING;
public class ModWorkManager {
private static final Logger LOGGER = LogManager.getLogger();
private static final long PARK_TIME = TimeUnit.MILLISECONDS.toNanos(1);
public interface DrivenExecutor extends Executor {
boolean selfDriven();
boolean driveOne();
default void drive(Runnable ticker) {
if (!selfDriven()) {
while (driveOne()) {
ticker.run();
ticker.run();
while (true) {
if (!driveOne()) break;
}
} else {
// park for a bit so other threads can schedule
@@ -32,8 +34,11 @@ public class ModWorkManager {
}
}
}
private static class SyncExecutor implements DrivenExecutor {
private ConcurrentLinkedDeque<Runnable> tasks = new ConcurrentLinkedDeque<>();
private record SyncExecutor(ConcurrentLinkedDeque<Runnable> tasks) implements DrivenExecutor {
public SyncExecutor() {
this(new ConcurrentLinkedDeque<>());
}
@Override
public boolean driveOne() {
@@ -55,13 +60,7 @@ public class ModWorkManager {
}
}
private static class WrappingExecutor implements DrivenExecutor {
private final Executor wrapped;
public WrappingExecutor(final Executor executor) {
this.wrapped = executor;
}
private record WrappingExecutor(Executor wrapped) implements DrivenExecutor {
@Override
public boolean selfDriven() {
return true;
@@ -78,11 +77,9 @@ public class ModWorkManager {
}
}
private static SyncExecutor syncExecutor;
private static final SyncExecutor syncExecutor = new SyncExecutor();
public static DrivenExecutor syncExecutor() {
if (syncExecutor == null)
syncExecutor = new SyncExecutor();
return syncExecutor;
}
@@ -90,14 +87,8 @@ public class ModWorkManager {
return new WrappingExecutor(executor);
}
private static ForkJoinPool parallelThreadPool;
public static Executor parallelExecutor() {
if (parallelThreadPool == null) {
final int loadingThreadCount = FMLConfig.loadingThreadCount();
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
parallelThreadPool = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
}
return parallelThreadPool;
return LazyInit.PARALLEL_EXECUTOR;
}
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
@@ -108,4 +99,14 @@ public class ModWorkManager {
return thread;
}
private static final class LazyInit {
private LazyInit() {}
private static final ForkJoinPool PARALLEL_EXECUTOR;
static {
final int loadingThreadCount = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.MAX_THREADS);
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
PARALLEL_EXECUTOR = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
}
}
}
@@ -23,7 +23,7 @@ public final class OptionalMod<T>
return new OptionalMod<>(modId);
}
private static OptionalMod<?> EMPTY = new OptionalMod<>(true);
private static final OptionalMod<?> EMPTY = new OptionalMod<>(true);
private static <T> OptionalMod<T> empty() {
@SuppressWarnings("unchecked")
OptionalMod<T> t = (OptionalMod<T>) EMPTY;
@@ -213,8 +213,8 @@ public final class OptionalMod<T>
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj instanceof OptionalMod) {
return Objects.equals(((OptionalMod)obj).modId, modId);
if (obj instanceof OptionalMod<?> optionalMod) {
return Objects.equals(optionalMod.modId, modId);
}
return false;
}
@@ -5,19 +5,30 @@
package net.minecraftforge.fml;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import java.util.Optional;
import java.util.function.Consumer;
public class StartupMessageManager {
public static void addModMessage(final String message) {
net.minecraftforge.fml.loading.progress.StartupMessageManager.addModMessage(message);
StartupNotificationManager.addModMessage(message);
}
public static Optional<Consumer<String>> modLoaderConsumer() {
return net.minecraftforge.fml.loading.progress.StartupMessageManager.modLoaderConsumer();
return StartupNotificationManager.modLoaderConsumer();
}
public static Optional<Consumer<String>> mcLoaderConsumer() {
return net.minecraftforge.fml.loading.progress.StartupMessageManager.mcLoaderConsumer();
return StartupNotificationManager.mcLoaderConsumer();
}
public static ProgressMeter addProgressBar(final String barName, final int count) {
return StartupNotificationManager.addProgressBar(barName, count);
}
public static ProgressMeter prependProgressBar(final String barName, final int count) {
return StartupNotificationManager.prependProgressBar(barName, count);
}
}
@@ -7,7 +7,6 @@ package net.minecraftforge.fml;
import com.google.gson.Gson;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.forgespi.language.IModInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -35,81 +34,72 @@ import java.util.zip.GZIPInputStream;
import static net.minecraftforge.fml.VersionChecker.Status.*;
public class VersionChecker
{
public class VersionChecker {
private static final Logger LOGGER = LogManager.getLogger();
private static final int MAX_HTTP_REDIRECTS = Integer.getInteger("http.maxRedirects", 20);
private static final int HTTP_TIMEOUT_SECS = Integer.getInteger("http.timeoutSecs", 15);
public enum Status
{
public enum Status {
PENDING(),
FAILED(),
UP_TO_DATE(),
OUTDATED(3, true),
OUTDATED(3, true, true),
AHEAD(),
BETA(),
BETA_OUTDATED(6, true);
BETA_OUTDATED(6, true, true);
final int sheetOffset;
final boolean draw, animated;
private final int sheetOffset;
private final boolean draw, animated;
Status()
{
Status() {
this(0, false, false);
}
Status(int sheetOffset)
{
this(sheetOffset, true, false);
}
Status(int sheetOffset, boolean animated)
{
this(sheetOffset, true, animated);
}
Status(int sheetOffset, boolean draw, boolean animated)
{
Status(int sheetOffset, boolean draw, boolean animated) {
this.sheetOffset = sheetOffset;
this.draw = draw;
this.animated = animated;
}
public int getSheetOffset()
{
public int getSheetOffset() {
return sheetOffset;
}
public boolean shouldDraw()
{
public boolean shouldDraw() {
return draw;
}
public boolean isAnimated()
{
public boolean isAnimated() {
return animated;
}
public boolean isOutdated() {
return this == OUTDATED || this == BETA_OUTDATED;
}
}
public record CheckResult(VersionChecker.Status status, ComparableVersion target, Map<ComparableVersion, String> changes, String url) {}
public static void startVersionCheck()
{
new Thread("Forge Version Check")
{
public static void startVersionCheck() {
new Thread("Forge Version Check") {
private HttpClient client;
private String mcVersion;
private String agent;
@Override
public void run()
{
if (!FMLConfig.runVersionCheck())
{
public void run() {
if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) {
LOGGER.info("Global Forge version check system disabled, no further processing.");
return;
}
var mc = ModList.get().getModContainerById("minecraft");
var forge = ModList.get().getModContainerById("forge");
this.mcVersion = mc.get().getModInfo().getVersion().toString();
this.agent = "Java-http-client/" + System.getProperty("java.version")
+ " MinecraftForge/" + mcVersion + '-' + forge.get().getModInfo().getVersion();
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS)).build();
gatherMods().forEach(this::process);
}
@@ -119,15 +109,9 @@ public class VersionChecker
*/
private String openUrlString(URL url, IModInfo mod) throws IOException, URISyntaxException, InterruptedException {
URL currentUrl = url;
var userAgent = this.agent + ' ' + mod.getModId() + '/' + mod.getVersion();
final StringBuilder sb = new StringBuilder();
sb.append("Java-http-client/").append(System.getProperty("java.version")).append(' ');
sb.append("MinecraftForge/").append(FMLLoader.versionInfo().mcAndForgeVersion()).append(' ');
sb.append(mod.getModId()).append('/').append(mod.getVersion());
final String userAgent = sb.toString();
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++)
{
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++) {
var request = HttpRequest.newBuilder()
.uri(currentUrl.toURI())
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS))
@@ -139,8 +123,7 @@ public class VersionChecker
final HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
int responseCode = response.statusCode();
if (responseCode >= 300 && responseCode <= 399)
{
if (responseCode >= 300 && responseCode <= 399) {
String newLocation = response.headers().firstValue("Location")
.orElseThrow(() -> new IOException("Got a 3xx response code but Location header was null while trying to fetch " + url));
currentUrl = new URL(currentUrl, newLocation);
@@ -150,26 +133,22 @@ public class VersionChecker
final boolean isGzipEncoded = response.headers().firstValue("Content-Encoding").orElse("").equals("gzip");
final String bodyStr;
try (InputStream inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body())
{
try (var bufferedReader = new BufferedReader(new InputStreamReader(inStream)))
{
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
}
try (var inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body();
var bufferedReader = new BufferedReader(new InputStreamReader(inStream))
) {
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
}
return bodyStr;
}
throw new IOException("Too many redirects while trying to fetch " + url);
}
private void process(IModInfo mod)
{
private void process(IModInfo mod) {
Status status = PENDING;
ComparableVersion target = null;
Map<ComparableVersion, String> changes = null;
String display_url = null;
try
{
try {
if (mod.getUpdateURL().isEmpty()) return;
URL url = mod.getUpdateURL().get();
LOGGER.info("[{}] Starting version check at {}", mod.getModId(), url.toString());
@@ -178,54 +157,40 @@ public class VersionChecker
LOGGER.debug("[{}] Received version check data:\n{}", mod.getModId(), data);
@SuppressWarnings("unchecked")
Map<String, Object> json = new Gson().fromJson(data, Map.class);
@SuppressWarnings("unchecked")
Map<String, String> promos = (Map<String, String>)json.get("promos");
display_url = (String)json.get("homepage");
var mcVersion = FMLLoader.versionInfo().mcVersion();
String rec = promos.get(mcVersion + "-recommended");
String lat = promos.get(mcVersion + "-latest");
ComparableVersion current = new ComparableVersion(mod.getVersion().toString());
if (rec != null)
{
if (rec != null) {
ComparableVersion recommended = new ComparableVersion(rec);
int diff = recommended.compareTo(current);
if (diff == 0)
status = UP_TO_DATE;
else if (diff < 0)
{
else if (diff < 0) {
status = AHEAD;
if (lat != null)
{
if (lat != null) {
ComparableVersion latest = new ComparableVersion(lat);
if (current.compareTo(latest) < 0)
{
if (current.compareTo(latest) < 0) {
status = OUTDATED;
target = latest;
}
}
}
else
{
} else {
status = OUTDATED;
target = recommended;
}
}
else if (lat != null)
{
} else if (lat != null) {
ComparableVersion latest = new ComparableVersion(lat);
if (current.compareTo(latest) < 0)
status = BETA_OUTDATED;
else
status = BETA;
status = current.compareTo(latest) < 0 ? BETA_OUTDATED : BETA;
target = latest;
}
else
} else
status = BETA;
LOGGER.info("[{}] Found status: {} Current: {} Target: {}", mod.getModId(), status, current, target);
@@ -233,38 +198,30 @@ public class VersionChecker
changes = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
Map<String, String> tmp = (Map<String, String>)json.get(mcVersion);
if (tmp != null)
{
if (tmp != null) {
List<ComparableVersion> ordered = new ArrayList<>();
for (String key : tmp.keySet())
{
for (String key : tmp.keySet()) {
ComparableVersion ver = new ComparableVersion(key);
if (ver.compareTo(current) > 0 && (target == null || ver.compareTo(target) < 1))
{
ordered.add(ver);
}
}
Collections.sort(ordered);
for (ComparableVersion ver : ordered)
{
for (ComparableVersion ver : ordered) {
changes.put(ver, tmp.get(ver.toString()));
}
}
}
catch (Exception e)
{
} catch (Exception e) {
LOGGER.warn("Failed to process update information", e);
status = FAILED;
}
results.put(mod, new CheckResult(status, target, changes, display_url));
RESULTS.put(mod, new CheckResult(status, target, changes, display_url));
}
}.start();
}
// Gather a list of mods that have opted in to this update system by providing a URL.
private static List<IModInfo> gatherMods()
{
private static List<IModInfo> gatherMods() {
List<IModInfo> ret = new LinkedList<>();
for (IModInfo info : ModList.get().getMods()) {
if (info.getUpdateURL().isPresent())
@@ -273,12 +230,10 @@ public class VersionChecker
return ret;
}
private static Map<IModInfo, CheckResult> results = new ConcurrentHashMap<>();
private static final Map<IModInfo, CheckResult> RESULTS = new ConcurrentHashMap<>();
private static final CheckResult PENDING_CHECK = new CheckResult(PENDING, null, null, null);
public static CheckResult getResult(IModInfo mod)
{
return results.getOrDefault(mod, PENDING_CHECK);
public static CheckResult getResult(IModInfo mod) {
return RESULTS.getOrDefault(mod, PENDING_CHECK);
}
}
@@ -14,6 +14,7 @@ import com.mojang.logging.LogUtils;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLPaths;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import java.io.IOException;
@@ -25,8 +26,65 @@ import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;
public class ConfigFileTypeHandler {
private static final Logger LOGGER = LogUtils.getLogger();
static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.defaultConfigPath());
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH));
private static final ConfigFileTypeHandler CLIENT = new ConfigFileTypeHandler(ModConfig.Type.CLIENT);
private static final ConfigFileTypeHandler COMMON = new ConfigFileTypeHandler(ModConfig.Type.COMMON);
private static final ConfigFileTypeHandler SERVER = new ConfigFileTypeHandler(ModConfig.Type.SERVER);
private final @Nullable ModConfig.Type type;
private @Nullable FileWatcher watcher;
// exists for bin compat
public ConfigFileTypeHandler() {
this(null);
}
private ConfigFileTypeHandler(@Nullable ModConfig.Type type) {
this.type = type;
}
/**
* Gets the handler for the given {@link ModConfig.Type}.
*
* @param type The type to get the handler for
* @return The handler
*/
static ConfigFileTypeHandler get(ModConfig.Type type) {
return switch (type) {
case CLIENT -> CLIENT;
case COMMON -> COMMON;
case SERVER -> SERVER;
};
}
/**
* Gets the {@link FileWatcher} for this handler, creating it if it doesn't exist and/or has been stopped.
*
* @return The watcher
*
* @apiNote This is package-private so modders can't just call {@link FileWatcher#stop()} and fuck up everything.
*/
FileWatcher getWatcher() {
if (this.watcher == null) {
LOGGER.debug(CONFIG, "Starting watcher for handler: {}", this);
this.watcher = new FileWatcher();
}
return this.watcher;
}
/**
* Stops the {@link FileWatcher} for this handler, and sets it to null afterward. Use this instead of
* {@link FileWatcher#stop()}.
*/
void stopWatcher() {
if (this.watcher == null) return;
LOGGER.debug(CONFIG, "Stopping watcher for hander: {}", this);
this.watcher.stop();
this.watcher = null;
}
public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {
return (c) -> {
@@ -37,7 +95,7 @@ public class ConfigFileTypeHandler {
onFileNotFound((newfile, configFormat)-> setupConfigFile(c, newfile, configFormat)).
writingMode(WritingMode.REPLACE).
build();
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath.toString());
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath);
try
{
configData.load();
@@ -46,13 +104,9 @@ public class ConfigFileTypeHandler {
{
throw new ConfigLoadingException(c, ex);
}
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath.toString());
try {
FileWatcher.defaultInstance().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath.toString());
} catch (IOException e) {
throw new RuntimeException("Couldn't watch config file", e);
}
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath);
this.getWatcher().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath);
return configData;
};
}
@@ -60,14 +114,16 @@ public class ConfigFileTypeHandler {
public void unload(Path configBasePath, ModConfig config) {
Path configPath = configBasePath.resolve(config.getFileName());
try {
FileWatcher.defaultInstance().removeWatch(configBasePath.resolve(config.getFileName()));
this.getWatcher().removeWatch(configPath);
} catch (RuntimeException e) {
LOGGER.error("Failed to remove config {} from tracker!", configPath.toString(), e);
LOGGER.error("Failed to remove config {} from tracker!", configPath, e);
}
}
private boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
Files.createDirectories(file.getParent());
private static boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
if (!Files.isDirectory(file.getParent())) {
Files.createDirectories(file.getParent());
}
Path p = defaultConfigPath.resolve(modConfig.getFileName());
if (Files.exists(p)) {
LOGGER.info(CONFIG, "Loading default config file from path {}", p);
@@ -111,17 +167,16 @@ public class ConfigFileTypeHandler {
}
}
private static class ConfigWatcher implements Runnable {
private final ModConfig modConfig;
private final CommentedFileConfig commentedFileConfig;
private final ClassLoader realClassLoader;
ConfigWatcher(final ModConfig modConfig, final CommentedFileConfig commentedFileConfig, final ClassLoader classLoader) {
this.modConfig = modConfig;
this.commentedFileConfig = commentedFileConfig;
this.realClassLoader = classLoader;
}
@Override
public String toString() {
return "ConfigFileTypeHandler[" + (type != null ? type : "UNKNOWN") + "]";
}
private record ConfigWatcher(
ModConfig modConfig,
CommentedFileConfig commentedFileConfig,
ClassLoader realClassLoader
) implements Runnable {
@Override
public void run() {
// Force the regular classloader onto the special thread
@@ -8,6 +8,9 @@ package net.minecraftforge.fml.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.mojang.logging.LogUtils;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.DistExecutor;
import net.minecraftforge.fml.loading.FMLPaths;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
@@ -47,15 +50,32 @@ public class ConfigTracker {
public void loadConfigs(ModConfig.Type type, Path configBasePath) {
LOGGER.debug(CONFIG, "Loading configs type {}", type);
this.configSets.get(type).forEach(config -> openConfig(config, configBasePath));
for (ModConfig config : this.configSets.get(type)) {
openConfig(config, configBasePath);
}
}
// TODO: [FML] This is only called for the server (outside of forceUnload)
// rethink config implementation for eventual FML rewrite
public void unloadConfigs(ModConfig.Type type, Path configBasePath) {
LOGGER.debug(CONFIG, "Unloading configs type {}", type);
this.configSets.get(type).forEach(config -> closeConfig(config, configBasePath));
for (ModConfig config : this.configSets.get(type)) {
closeConfig(config, configBasePath);
}
ConfigFileTypeHandler.get(type).stopWatcher();
}
private void openConfig(final ModConfig config, final Path configBasePath) {
// If there is a better way to do this, please tell me. Because this is FUCKED
public void forceUnload() {
// This is how ModStateProvider handles loading configs. So we're doing the same but with unloadConfigs instead
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> this.unloadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get()));
this.unloadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get());
// just in case server watcher is still alive somehow...
ConfigFileTypeHandler.get(ModConfig.Type.SERVER).stopWatcher();
}
private static void openConfig(final ModConfig config, final Path configBasePath) {
LOGGER.trace(CONFIG, "Loading config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
final CommentedFileConfig configData = config.getHandler().reader(configBasePath).apply(config);
config.setConfigData(configData);
@@ -63,7 +83,7 @@ public class ConfigTracker {
config.save();
}
private void closeConfig(final ModConfig config, final Path configBasePath) {
private static void closeConfig(final ModConfig config, final Path configBasePath) {
if (config.getConfigData() != null) {
LOGGER.trace(CONFIG, "Closing config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
// stop the filewatcher before we save the file and close it, so reload doesn't fire
@@ -10,17 +10,11 @@ import net.minecraftforge.fml.Bindings;
import java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
public interface IConfigEvent {
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {
@Deprecated(since = "1.19.3", forRemoval = true)
@ApiStatus.Internal
ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading) {
this(loading, reloading, null);
}
}
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {}
ConfigConfig CONFIGCONFIG = Bindings.getConfigConfiguration().get();
static IConfigEvent reloading(ModConfig modConfig) {
@@ -24,14 +24,13 @@ public class ModConfig
private final ModContainer container;
private final ConfigFileTypeHandler configHandler;
private CommentedConfig configData;
private Callable<Void> saveHandler;
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer container, final String fileName) {
this.type = type;
this.spec = spec;
this.fileName = fileName;
this.container = container;
this.configHandler = ConfigFileTypeHandler.TOML;
this.configHandler = ConfigFileTypeHandler.get(type);
ConfigTracker.INSTANCE.trackConfig(this);
}
+92
View File
@@ -0,0 +1,92 @@
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
apply from: rootProject.file('build_shared.gradle')
import org.gradle.internal.os.OperatingSystem
switch (OperatingSystem.current()) {
case OperatingSystem.LINUX:
project.ext.lwjglNatives = "natives-linux"
break
case OperatingSystem.MAC_OS:
project.ext.lwjglNatives = "natives-macos"
break
case OperatingSystem.WINDOWS:
project.ext.lwjglNatives = "natives-windows"
break
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
dependencies {
compileOnly(libs.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")
}
tasks.named('test', Test).configure {
useJUnitPlatform()
}
tasks.named('jar', Jar).configure {
manifest {
attributes([
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
'Forge-Module-Layer': 'boot'
] as LinkedHashMap)
attributes([
'Specification-Title': 'FML Early Display',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML Early Display',
'Implementation-Vendor': 'Forge Development LLC',
'Implementation-Version': '1.0'
] as LinkedHashMap, 'net/minecraftforge/fml/earlydisplay/')
}
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
}
publishing {
publications.register('mavenJava', MavenPublication).configure {
from components.java
artifactId = 'fmlearlydisplay'
pom {
name = project.name
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
url = 'https://github.com/MinecraftForge/MinecraftForge'
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
license PomUtils.Licenses.LGPLv2_1
}
}
repositories {
maven gradleutils.publishingForgeMaven
}
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
public enum ColourScheme {
RED(new Colour(239, 50, 61), new Colour(255, 255, 255)),
BLACK(new Colour(0, 0, 0), new Colour(255, 255, 255));
private final Colour background;
private final Colour foreground;
ColourScheme(final Colour background, final Colour foreground) {
this.background = background;
this.foreground = foreground;
}
public Colour background() {
return background;
}
public Colour foreground() {
return foreground;
}
public record Colour(int red, int green, int blue) {
public float redf() {
return ((float)red)/255f;
}
public float greenf() {
return ((float)green)/255f;
}
public float bluef() {
return ((float)blue)/255f;
}
public int packedint(int a) {
return ((a & 0xff) << 24) | ((blue & 0xff) << 16) | ((green & 0xff) << 8) | (red & 0xff);
}
}
}
@@ -0,0 +1,653 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import joptsimple.OptionParser;
import net.minecraftforge.fml.loading.FMLConfig;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
import net.minecraftforge.fml.loading.ImmediateWindowProvider;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL.createCapabilities;
import static org.lwjgl.opengl.GL32C.*;
/**
* The Loading Window that is opened Immediately after Forge starts.
* It is called from the ModDirTransformerDiscoverer, the soonest method that ModLauncher calls into Forge code.
* In this way, we can be sure that this will not run before any transformer or injection.
*
* The window itself is spun off into a secondary thread, and is handed off to the main game by Forge.
*
* Because it is created so early, this thread will "absorb" the context from OpenGL.
* Therefore, it is of utmost importance that the Context is made Current for the main thread before handoff,
* otherwise OS X will crash out.
*
* Based on the prior ClientVisualization, with some personal touches.
*/
public class DisplayWindow implements ImmediateWindowProvider {
private static final int[][] GL_VERSIONS = new int[][] {{4,6}, {4,5}, {4,4}, {4,3}, {4,2}, {4,1}, {4,0}, {3,3}, {3,2}};
private static final Logger LOGGER = LoggerFactory.getLogger("EARLYDISPLAY");
private final AtomicBoolean animationTimerTrigger = new AtomicBoolean(true);
private ColourScheme colourScheme = ColourScheme.RED;
private ElementShader elementShader;
private RenderElement.DisplayContext context;
private List<RenderElement> elements;
private int framecount;
private EarlyFramebuffer framebuffer;
private ScheduledFuture<?> windowTick;
private ScheduledFuture<?> initializationFuture;
private PerformanceInfo performanceInfo;
@SuppressWarnings("unused")
private ScheduledFuture<?> performanceTick;
// The GL ID of the window. Used for all operations
private long window;
// The thread that contains and ticks the window while Forge is loading mods
private ScheduledExecutorService renderScheduler;
private int fbWidth;
private int fbHeight;
private int fbScale;
private int winWidth;
private int winHeight;
private int winX;
private int winY;
private final Semaphore renderLock = new Semaphore(1);
private boolean maximized;
private String glVersion;
private SimpleFont font;
private Runnable repaintTick = ()->{};
@Override
public String name() {
return "fmlearlywindow";
}
@Override
public Runnable initialize(String[] arguments) {
String mcVersion = FMLLoader.versionInfo().mcVersion();
String forgeVersion = FMLLoader.versionInfo().forgeVersion();
final OptionParser parser = new OptionParser();
var widthopt = parser.accepts("width")
.withRequiredArg().ofType(Integer.class)
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH));
var heightopt = parser.accepts("height")
.withRequiredArg().ofType(Integer.class)
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT));
var maximizedopt = parser.accepts("earlywindow.maximized");
parser.allowsUnrecognizedOptions();
var parsed = parser.parse(arguments);
winWidth = parsed.valueOf(widthopt);
winHeight = parsed.valueOf(heightopt);
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH, winWidth);
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT, winHeight);
fbScale = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_FBSCALE);
if (System.getenv("FML_EARLY_WINDOW_DARK") != null) {
this.colourScheme = ColourScheme.BLACK;
} else {
try {
// check the options file for the colour scheme
var optionLines = Files.readAllLines(FMLPaths.GAMEDIR.get().resolve(Path.of("options.txt")));
var keyName = "darkMojangStudiosBackground:";
for (String line : optionLines) {
if (line.startsWith(keyName)) {
this.colourScheme = line.startsWith("true", keyName.length()) ? ColourScheme.BLACK : ColourScheme.RED;
break;
}
}
} catch (IOException e) {
this.colourScheme = ColourScheme.RED; // fallback to red colourScheme
}
}
this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED);
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + forgeVersion));
performanceInfo = new PerformanceInfo();
return start(mcVersion, forgeVersion);
}
private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer
private long nextFrameTime = 0;
/**
* The main render loop.
* renderThread executes this.
*
* Performs initialization and then ticks the screen at 20 fps.
* When the thread is killed, context is destroyed.
*/
private void renderThreadFunc() {
if (!renderLock.tryAcquire()) {
return;
}
try {
long nt;
if ((nt = System.nanoTime()) < nextFrameTime) {
return;
}
nextFrameTime = nt + MINFRAMETIME;
glfwMakeContextCurrent(window);
framebuffer.activate();
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
this.context.elementShader().activate();
this.context.elementShader().updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
paintFramebuffer();
this.context.elementShader().clear();
framebuffer.deactivate();
glViewport(0, 0, fbWidth, fbHeight);
framebuffer.draw(this.fbWidth, this.fbHeight);
// Swap buffers; we're done
glfwSwapBuffers(window);
} catch (Throwable t) {
LOGGER.error("BARF", t);
} finally {
if (this.windowTick != null) glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread
renderLock.release();
}
}
/**
* Render initialization methods called by the Render Thread.
* It compiles the fragment and vertex shaders for rendering text with STB, and sets up basic render framework.
*
* Nothing fancy, we just want to draw and render text.
*/
private void initRender(final @Nullable String mcVersion, final String forgeVersion) {
// This thread owns the GL render context now. We should make a note of that.
glfwMakeContextCurrent(window);
// Wait for one frame to be complete before swapping; enable vsync in other words.
glfwSwapInterval(1);
createCapabilities();
LOGGER.info("GL info: "+ glGetString(GL_RENDERER) + " GL version " + glGetString(GL_VERSION) + ", " + glGetString(GL_VENDOR));
elementShader = new ElementShader();
try {
elementShader.init();
} catch (Throwable t) {
LOGGER.error("Crash during shader initialization", t);
crashElegantly("An error occurred initializing shaders.");
}
// Set the clear color based on the colour scheme
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
// we always render to an 854x480 texture and then fit that to the screen - with a scale factor
this.context = new RenderElement.DisplayContext(854, 480, fbScale, elementShader, colourScheme, performanceInfo);
framebuffer = new EarlyFramebuffer(this.context);
try {
this.font = new SimpleFont("Monocraft.ttf", fbScale, 200000, 1 + RenderElement.INDEX_TEXTURE_OFFSET);
} catch (Throwable t) {
LOGGER.error("Crash during font initialization", t);
crashElegantly("An error occurred initializing a font for rendering. "+t.getMessage());
}
this.elements = new ArrayList<>(List.of(
RenderElement.anvil(font),
RenderElement.logMessageOverlay(font),
RenderElement.forgeVersionOverlay(font, mcVersion + "-" + forgeVersion),
RenderElement.performanceBar(font),
RenderElement.progressBars(font)
));
var date = Calendar.getInstance();
if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1))
this.elements.add(0, RenderElement.squir());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glfwMakeContextCurrent(0);
this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS);
this.performanceTick = renderScheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS);
// schedule a 50 ms ticker to try and smooth out the rendering
renderScheduler.scheduleAtFixedRate(()-> animationTimerTrigger.set(true), 1, 50, TimeUnit.MILLISECONDS);
}
/**
* Called every frame by the Render Thread to draw to the screen.
*/
void paintFramebuffer() {
// Clear the screen to our color
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (var itr = this.elements.iterator(); itr.hasNext(); ) {
var element = itr.next();
if (!element.render(context, framecount))
itr.remove();
}
if (animationTimerTrigger.compareAndSet(true, false)) // we only increment the framecount on a periodic basis
framecount++;
}
public void render(int alpha) {
var currentVAO = glGetInteger(GL_VERTEX_ARRAY_BINDING);
var currentFB = glGetInteger(GL_READ_FRAMEBUFFER_BINDING);
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
RenderElement.globalAlpha = alpha;
framebuffer.activate();
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), alpha / 255f);
elementShader.activate();
elementShader.updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
paintFramebuffer();
elementShader.clear();
framebuffer.deactivate();
glBindVertexArray(currentVAO);
glBindFramebuffer(GL_FRAMEBUFFER, currentFB);
}
/**
* Start the window and Render Thread; we're ready to go.
*/
public Runnable start(@Nullable String mcVersion, final String forgeVersion) {
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;
}
private static final String ERROR_URL = "https://links.minecraftforge.net/early-display-errors";
@Override
public String getGLVersion() {
return this.glVersion;
}
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");
msgBuilder.append("Failure details:\n");
msgBuilder.append(errorDetails);
msgBuilder.append("\n\n");
msgBuilder.append("If you click yes, we will try and open " + ERROR_URL + " in your default browser");
LOGGER.error("ERROR DISPLAY\n"+msgBuilder);
// we show the display on a new dedicated thread
Executors.newSingleThreadExecutor().submit(()-> {
var res = TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", msgBuilder.toString(), "yesno", "error", false);
if (res) {
try {
Desktop.getDesktop().browse(URI.create(ERROR_URL));
} catch (IOException ioe) {
TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", "Sadly, we couldn't open your browser.\nVisit " + ERROR_URL, "ok", "error", false);
}
}
System.exit(1);
});
}
/**
* Called to initialize the window when preparing for the Render Thread.
*
* The act of calling glfwInit here creates a concurrency issue; GL doesn't know whether we're gonna call any
* GL functions from the secondary thread and the main thread at the same time.
*
* It's then our job to make sure this doesn't happen, only calling GL functions where the Context is Current.
* As long as we can verify that, then GL (and things like OS X) have no complaints with doing this.
*
* @param mcVersion Minecraft Version
* @return The selected GL profile as an integer pair
*/
public void initWindow(@Nullable String mcVersion) {
// Initialize GLFW with a time guard, in case something goes wrong
long glfwInitBegin = System.nanoTime();
if (!glfwInit()) {
crashElegantly("We are unable to initialize the graphics system.\nglfwInit failed.\n");
throw new IllegalStateException("Unable to initialize GLFW");
}
long glfwInitEnd = System.nanoTime();
if (glfwInitEnd - glfwInitBegin > 1e9) {
LOGGER.error("WARNING : glfwInit took {} seconds to start.", (glfwInitEnd - glfwInitBegin) / 1.0e9);
}
// Clear the Last Exception (#7285 - Prevent Vanilla throwing an IllegalStateException due to invalid controller mappings)
handleLastGLFWError((error, description) -> LOGGER.error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description)));
// Set window hints for the new window we're gonna create.
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
String vanillaWindowTitle = "Minecraft* ";
if (mcVersion != null) vanillaWindowTitle += mcVersion;
// this emulates what we would get without early progress window
// as vanilla never sets these, so GLFW uses the first window title
// set them explicitly to avoid it using "FML early loading progress" as the class
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
long primaryMonitor = glfwGetPrimaryMonitor();
if (primaryMonitor == 0) {
LOGGER.error("Failed to find a primary monitor - this means LWJGL isn't working properly");
crashElegantly("Failed to locate a primary monitor.\nglfwGetPrimaryMonitor failed.\n");
throw new IllegalStateException("Can't find a primary monitor");
}
GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor);
if (vidmode == null) {
LOGGER.error("Failed to get the current display video mode.");
crashElegantly("Failed to get current display resolution.\nglfwGetVideoMode failed.\n");
throw new IllegalStateException("Can't get a resolution");
}
long window = 0;
var successfulWindow = new AtomicBoolean(false);
var windowFailFuture = renderScheduler.schedule(()->{
if (!successfulWindow.get()) crashElegantly("Timed out trying to setup the Game Window.");
}, 10, TimeUnit.SECONDS);
int versidx = 0;
var skipVersions = FMLConfig.<String>getListConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SKIP_GL_VERSIONS);
boolean showHelpLog = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG);
final String[] lastGLError = new String[GL_VERSIONS.length];
do {
final var glVersionToTry = GL_VERSIONS[versidx][0] + "." + GL_VERSIONS[versidx][1];
if (skipVersions.contains(glVersionToTry)) {
LOGGER.info("Skipping GL version "+ glVersionToTry+" because of configuration");
versidx++;
continue;
}
LOGGER.info("Trying GL version " + glVersionToTry);
if (showHelpLog && versidx == 0) {
LOGGER.info("""
If this message is the only thing at the bottom of your log before a crash, you probably have a driver issue.
Possible solutions:
A) Make sure Minecraft is set to prefer high performance graphics in the OS and/or driver control panel
B) Check for driver updates on the graphics brand's website
C) Try reinstalling your graphics drivers
D) If still not working after trying all of the above, ask for further help on the Forge forums or Discord
You can safely ignore this message if the game starts up successfully.""");
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSIONS[versidx][0]); // we try our versions one at a time
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSIONS[versidx][1]);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
window = glfwCreateWindow(winWidth, winHeight, vanillaWindowTitle, 0L, 0L);
var erridx = versidx;
handleLastGLFWError((error, description) -> lastGLError[erridx] = String.format("Trying %d.%d: GLFW error: [0x%X]%s", GL_VERSIONS[erridx][0], GL_VERSIONS[erridx][1], error, description));
if (lastGLError[versidx] != null) {
LOGGER.trace(lastGLError[versidx]);
}
versidx++;
} while (window == 0 && versidx < GL_VERSIONS.length);
// LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(12));
if (versidx== GL_VERSIONS.length && window == 0) {
LOGGER.error("Failed to find any valid GLFW profile. "+lastGLError[0]);
crashElegantly("Failed to find a valid GLFW profile.\nWe tried "+
Arrays.stream(GL_VERSIONS).map(p->p[0]+"."+p[1]).filter(o -> !skipVersions.contains(o))
.collect(Collector.of(()->new StringJoiner(", ").setEmptyValue("no versions"), StringJoiner::add, StringJoiner::merge, StringJoiner::toString))+
" but none of them worked.\n"+ Arrays.stream(lastGLError).filter(Objects::nonNull).collect(Collectors.joining("\n")));
throw new IllegalStateException("Failed to create a GLFW window with any profile");
}
successfulWindow.set(true);
if (!windowFailFuture.cancel(true)) throw new IllegalStateException("We died but didn't somehow?");
var requestedVersion = GL_VERSIONS[versidx-1][0]+"."+GL_VERSIONS[versidx-1][1];
var maj = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);
var min = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);
var gotVersion = maj+"."+min;
LOGGER.info("Requested GL version "+requestedVersion+" got version "+gotVersion);
this.glVersion = gotVersion;
this.window = window;
if (showHelpLog)
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG, false);
int[] x = new int[1];
int[] y = new int[1];
glfwGetMonitorPos(primaryMonitor, x, y);
int monitorX = x[0];
int monitorY = y[0];
// glfwSetWindowSizeLimits(window, 854, 480, GLFW_DONT_CARE, GLFW_DONT_CARE);
if (this.maximized) {
glfwMaximizeWindow(window);
}
glfwGetWindowSize(window, x, y);
this.winWidth = x[0];
this.winHeight = y[0];
glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY);
// Attempt setting the icon
// int[] channels = new int[1];
// try (var glfwImgBuffer = GLFWImage.create(MemoryUtil.getAllocator().malloc(GLFWImage.SIZEOF), 1)) {
// final ByteBuffer imgBuffer;
// try (GLFWImage glfwImages = GLFWImage.malloc()) {
// imgBuffer = STBHelper.loadImageFromClasspath("forge_logo.png", 20000, x, y, channels);
// glfwImgBuffer.put(glfwImages.set(x[0], y[0], imgBuffer));
// glfwSetWindowIcon(window, glfwImgBuffer);
// STBImage.stbi_image_free(imgBuffer);
// }
// } catch (NullPointerException e) {
// System.err.println("Failed to load forge logo");
// }
// handleLastGLFWError((error, description) -> LOGGER.debug(String.format("Suppressing GLFW icon error: [0x%X]%s", error, description)));
glfwSetFramebufferSizeCallback(window, this::fbResize);
glfwSetWindowPosCallback(window, this::winMove);
glfwSetWindowSizeCallback(window, this::winResize);
// Show the window
glfwShowWindow(window);
glfwGetWindowPos(window, x, y);
this.winX = x[0];
this.winY = y[0];
glfwGetFramebufferSize(window, x, y);
this.fbWidth = x[0];
this.fbHeight = y[0];
glfwPollEvents();
}
private void winResize(long window, int width, int height) {
if (window == this.window && width != 0 && height != 0) {
this.winWidth = width;
this.winHeight = height;
}
}
private void fbResize(long window, int width, int height) {
if (window == this.window && width != 0 && height != 0) {
this.fbWidth = width;
this.fbHeight = height;
}
}
private void winMove(long window, int x, int y) {
if (window == this.window) {
this.winX = x;
this.winY = y;
}
}
private void handleLastGLFWError(BiConsumer<Integer, String> handler) {
try (MemoryStack memorystack = MemoryStack.stackPush()) {
PointerBuffer pointerbuffer = memorystack.mallocPointer(1);
int error = glfwGetError(pointerbuffer);
if (error != GLFW_NO_ERROR) {
long pDescription = pointerbuffer.get();
String description = pDescription == 0L ? "" : MemoryUtil.memUTF8(pDescription);
handler.accept(error, description);
}
}
}
/**
* Hand-off the window to the vanilla game.
* Called on the main thread instead of the game's initialization.
*
* @return the Window we own.
*/
public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitorSupplier) {
// wait for the window to actually be initialized
try {
this.initializationFuture.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
Thread.dumpStack();
crashElegantly("We seem to be having trouble initializing the window, waited for 30 seconds");
}
// we have to spin wait for the window ticker
ImmediateWindowHandler.updateProgress("Initializing Game Graphics");
while (!this.windowTick.isDone()) {
this.windowTick.cancel(false);
}
var tries = 0;
var renderlockticket = false;
do {
try {
renderlockticket = renderLock.tryAcquire(100, TimeUnit.MILLISECONDS);
if (++tries > 9) {
Thread.dumpStack();
crashElegantly("We seem to be having trouble handing off the window, tried for 1 second");
}
} catch (InterruptedException e) {
Thread.interrupted();
}
} while (!renderlockticket);
// we don't want the lock, just making sure it's back on the main thread
renderLock.release();
glfwMakeContextCurrent(window);
// Set the title to what the game wants
glfwSetWindowTitle(window, title.get());
glfwSwapInterval(0);
// Clean up our hooks
glfwSetFramebufferSizeCallback(window, null).free();
glfwSetWindowPosCallback(window, null).free();
glfwSetWindowSizeCallback(window, null).free();
this.repaintTick = this::renderThreadFunc; // the repaint will continue to be called until the overlay takes over
this.windowTick = null; // this tells the render thread that the async ticker is done
return window;
}
@Override
public boolean positionWindow(final Optional<Object> monitor, final IntConsumer widthSetter, final IntConsumer heightSetter, final IntConsumer xSetter, final IntConsumer ySetter) {
widthSetter.accept(this.winWidth);
heightSetter.accept(this.winHeight);
xSetter.accept(this.winX);
ySetter.accept(this.winY);
return true;
}
@Override
public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) {
width.accept(this.fbWidth);
height.accept(this.fbHeight);
}
private Method loadingOverlay;
@SuppressWarnings("unchecked")
@Override
public <T> Supplier<T> loadingOverlay(final Supplier<?> mc, final Supplier<?> ri, final Consumer<Optional<Throwable>> ex, final boolean fade) {
try {
return (Supplier<T>)loadingOverlay.invoke(null, mc, ri, ex, this);
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
}
}
@Override
public void updateModuleReads(final ModuleLayer layer) {
final var FORGE_MODULE = "net.minecraftforge.forge";
var forge_module = layer.findModule(FORGE_MODULE).orElse(null);
if (forge_module == null)
throw new IllegalStateException("Could not find " + FORGE_MODULE + " in " + layer);
getClass().getModule().addReads(forge_module);
var clz = Class.forName(forge_module, "net.minecraftforge.client.loading.ForgeLoadingOverlay");
for (var mtd : clz.getDeclaredMethods()) {
if (Modifier.isStatic(mtd.getModifiers()) && "newInstance".equals(mtd.getName())) {
this.loadingOverlay = mtd;
break;
}
}
if (loadingOverlay == null)
throw new IllegalStateException("Could not find static newInstace method in " + clz.getName());
}
public int getFramebufferTextureId() {
return framebuffer.getTexture();
}
public RenderElement.DisplayContext context() {
return this.context;
}
@Override
public void periodicTick() {
glfwPollEvents();
repaintTick.run();
}
public void addMojangTexture(final int textureId) {
this.elements.add(0, RenderElement.mojang(textureId, framecount));
}
public void close() {
// Close the Render Scheduler thread
renderScheduler.shutdown();
this.framebuffer.close();
this.context.elementShader().close();
SimpleBufferBuilder.destroy();
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import java.nio.IntBuffer;
import static net.minecraftforge.fml.earlydisplay.RenderElement.clamp;
import static org.lwjgl.opengl.GL32C.*;
public class EarlyFramebuffer {
private final int framebuffer;
private final int texture;
private final RenderElement.DisplayContext context;
EarlyFramebuffer(final RenderElement.DisplayContext context) {
this.context = context;
this.framebuffer = glGenFramebuffers();
this.texture = glGenTextures();
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, this.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, context.width() * context.scale(), context.height() * context.scale(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer)null);
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void activate() {
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
}
void deactivate() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void draw(int windowFBWidth, int windowFBHeight) {
var wscale = ((float)windowFBWidth / this.context.width());
var hscale = ((float)windowFBHeight / this.context.height());
var scale = this.context.scale() * Math.min(wscale, hscale) / 2f;
var wleft = (int)(windowFBWidth * 0.5f - scale * this.context.width());
var wtop = (int)(windowFBHeight * 0.5f - scale * this.context.height());
var wright = (int)(windowFBWidth * 0.5f + scale * this.context.width());
var wbottom = (int)(windowFBHeight * 0.5f + scale * this.context.height());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, this.framebuffer);
final var colour = this.context.colourScheme().background();
glClearColor(colour.redf(), colour.greenf(), colour.bluef(), 1f);
glClear(GL_COLOR_BUFFER_BIT);
// src Y are flipped, since our FB is flipped
glBlitFramebuffer(0, this.context.height() * this.context.scale(), this.context.width() * this.context.scale(), 0, clamp(wleft, 0, windowFBWidth), clamp(wtop, 0, windowFBHeight), clamp(wright, 0, windowFBWidth), clamp(wbottom, 0, windowFBHeight), GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
int getTexture() {
return this.texture;
}
public void close() {
glDeleteTextures(this.texture);
glDeleteFramebuffers(this.framebuffer);
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import static org.lwjgl.opengl.GL32C.*;
public class ElementShader {
private int program;
private int textureUniform;
private int screenSizeUniform;
private int renderTypeUniform;
public void init() {
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
// Bind the source of our shaders to the ones created above
glShaderSource(fragmentShader, """
#version 150 core
uniform sampler2D tex;
uniform int rendertype;
in vec2 fTex;
in vec4 fColour;
out vec4 fragColor;
void main() {
if (rendertype == 0)
fragColor = vec4(1,1,1,texture(tex, fTex).r) * fColour;
if (rendertype == 1)
fragColor = texture(tex, fTex) * fColour;
if (rendertype == 2)
fragColor = fColour;
}
""");
glShaderSource(vertexShader, """
#version 150 core
in vec2 position;
in vec2 tex;
in vec4 colour;
uniform vec2 screenSize;
out vec2 fTex;
out vec4 fColour;
void main() {
fTex = tex;
fColour = colour;
gl_Position = vec4((position/screenSize) * 2 - 1, 0.0, 1.0);
}
""");
// Compile the vertex and fragment elementShader so that we can use them
glCompileShader(vertexShader);
if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) {
throw new IllegalStateException("VertexShader linkage failure. \n" + glGetShaderInfoLog(vertexShader));
}
glCompileShader(fragmentShader);
if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) {
throw new IllegalStateException("FragmentShader linkage failure. \n" + glGetShaderInfoLog(fragmentShader));
}
var program = glCreateProgram();
glBindAttribLocation(program, 0, "position");
glBindAttribLocation(program, 1, "tex");
glBindAttribLocation(program, 2, "colour");
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) {
throw new RuntimeException("ShaderProgram linkage failure. \n" + glGetProgramInfoLog(program));
}
this.program = program;
glDetachShader(program, vertexShader);
glDetachShader(program, fragmentShader);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
textureUniform = glGetUniformLocation(program, "tex");
screenSizeUniform = glGetUniformLocation(program, "screenSize");
renderTypeUniform = glGetUniformLocation(program, "rendertype");
activate();
}
public void activate() {
glUseProgram(program);
}
public void updateTextureUniform(int textureNumber) {
glUniform1i(textureUniform, textureNumber);
}
public void updateScreenSizeUniform(int width, int height) {
glUniform2f(screenSizeUniform, width, height);
}
public void updateRenderTypeUniform(RenderType type) {
glUniform1i(renderTypeUniform, type.ordinal());
}
public void clear() {
glUseProgram(0);
}
public void close() {
glDeleteProgram(program);
}
public enum RenderType {
FONT, TEXTURE, BAR;
}
public int program() {
return program;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import com.sun.management.OperatingSystemMXBean;
import net.minecraftforge.fml.loading.FMLConfig;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
public class PerformanceInfo {
private static final MemoryMXBean MEMORY_BEAN = ManagementFactory.getMemoryMXBean();
private final boolean showCPUUsage;
private final OperatingSystemMXBean osBean;
private float memory;
private String text;
PerformanceInfo() {
showCPUUsage = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SHOW_CPU);
osBean = showCPUUsage ? ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class) : null;
}
void update() {
final MemoryUsage heapusage = MEMORY_BEAN.getHeapMemoryUsage();
memory = (float) heapusage.getUsed() / heapusage.getMax();
if (!showCPUUsage) {
text = "Memory: %d/%dMB (%.1f%%)".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f);
return;
}
var cpuLoad = osBean.getProcessCpuLoad();
String cpuText;
if (cpuLoad == -1) {
cpuText = "*CPU: %.1f%%".formatted(osBean.getCpuLoad() * 100f);
} else {
cpuText = "CPU: %.1f%%".formatted(cpuLoad * 100f);
}
text = "Memory: %d/%dMB (%.1f%%) %s".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f, cpuText);
}
String text() {
return text;
}
float memory() {
return memory;
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
public class QuadHelper {
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1, int colour) {
bb.pos(x0, y0).tex(u0, v0).colour(colour).endVertex();
bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex();
bb.pos(x0, y1).tex(u0, v1).colour(colour).endVertex();
bb.pos(x1, y1).tex(u1, v1).colour(colour).endVertex();
}
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1) {
bb.pos(x0, y0).tex(u0, v0).endVertex();
bb.pos(x1, y0).tex(u1, v0).endVertex();
bb.pos(x0, y1).tex(u0, v1).endVertex();
bb.pos(x1, y1).tex(u1, v1).endVertex();
}
}
@@ -0,0 +1,350 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import net.minecraftforge.fml.loading.progress.Message;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import static org.lwjgl.opengl.GL32C.*;
public class RenderElement {
static final int INDEX_TEXTURE_OFFSET = 5;
private final SimpleBufferBuilder bb;
private final Renderer renderer;
static int globalAlpha = 255;
private int retireCount;
interface Renderer {
void accept(SimpleBufferBuilder bb, DisplayContext context, int frame);
default Renderer then(Renderer r) {
if (r == null) return this;
return (bb, ctx, frame) -> {
r.accept(bb, ctx, frame);
this.accept(bb, ctx, frame);
};
}
}
@FunctionalInterface
interface TextureRenderer {
void accept(SimpleBufferBuilder bb, DisplayContext context, int[] size, int frame);
}
@FunctionalInterface
interface Initializer extends Supplier<Renderer> {}
@FunctionalInterface
interface TextGenerator {
void accept(SimpleBufferBuilder bb, SimpleFont fh, DisplayContext ctx);
}
public record DisplayContext(int width, int height, int scale, ElementShader elementShader, ColourScheme colourScheme, PerformanceInfo performance) {
public int scaledWidth() {
return scale() * width();
}
public int scaledHeight() {
return scale() * height();
}
}
public RenderElement(final Initializer rendererInitializer) {
this.bb = new SimpleBufferBuilder(1);
this.renderer = rendererInitializer.get();
}
public boolean render(DisplayContext ctx, int count) {
this.renderer.accept(bb, ctx, count);
return this.retireCount == 0 || this.retireCount < count;
}
public void retire(final int frame) {
this.retireCount = frame;
}
private static void startupLogMessages(SimpleBufferBuilder bb, SimpleFont font, DisplayContext context) {
List<StartupNotificationManager.AgeMessage> messages = StartupNotificationManager.getMessages();
List<SimpleFont.DisplayText> texts = new ArrayList<>();
for (int i = messages.size() - 1; i >= 0; i--) {
final StartupNotificationManager.AgeMessage pair = messages.get(i);
final float fade = clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f);
if (fade <0.01f) continue;
Message msg = pair.message();
int colour = Math.min((int)(fade * 255f), globalAlpha) << 24 | 0xFFFFFF;
texts.add(new SimpleFont.DisplayText(msg.getText()+"\n", colour));
}
font.generateVerticesForTexts(10, context.scaledHeight() - texts.size() * font.lineSpacing() + font.descent() - 10, bb, texts.toArray(SimpleFont.DisplayText[]::new));
}
public static RenderElement 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) -> {
var size = 256 * ctx.scale();
var x0 = (ctx.scaledWidth() - 2 * size) / 2;
var y0 = 64 * ctx.scale() + 32;
ctx.elementShader().updateTextureUniform(0);
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
var fade = Math.min((frame - frameStart) * 10, 255);
glBindTexture(GL_TEXTURE_2D, textureId);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, (fade << 24) | 0xFFFFFF);
QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, (fade << 24) | 0xFFFFFF);
bb.draw();
glBindTexture(GL_TEXTURE_2D, 0);
});
}
public static RenderElement logMessageOverlay(SimpleFont font) {
return new RenderElement(RenderElement.initializeText(font, RenderElement::startupLogMessages));
}
public static RenderElement forgeVersionOverlay(SimpleFont font, String version) {
return new RenderElement(RenderElement.initializeText(font, (bb, fnt, ctx)->
font.generateVerticesForTexts(ctx.scaledWidth() - font.stringWidth(version) - 10,
ctx.scaledHeight() - font.lineSpacing() + font.descent() - 10, bb,
new SimpleFont.DisplayText(version, ctx.colourScheme.foreground().packedint(RenderElement.globalAlpha)))));
}
public static RenderElement squir() {
return new RenderElement(RenderElement.initializeTexture("squirrel.png", 45000, 3, (bb, context, size, frame) -> {
var inset = 5f;
var x0 = inset;
var x1 = inset + size[0] * context.scale();
var y0 = inset;
var y1 = inset + size[1] * context.scale();
int fade = (int) (Math.cos(frame * Math.PI / 16) * 16) + 16;
// int fade = 0xff;
var colour = (Math.min(fade, globalAlpha) & 0xff) << 24 | 0xffffff;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, 0f, 1f, colour);
}));
}
public static RenderElement anvil(SimpleFont font) {
return new RenderElement(RenderElement.initializeTexture("forge_anvil.png", 20000, 2, (bb, context, size, frame) -> {
var x0 = context.scaledWidth() - size[0] * context.scale();
var x1 = context.scaledWidth();
var y0 = context.scaledHeight() - size[0] * context.scale() - font.descent() - font.lineSpacing();
var y1 = context.scaledHeight() - font.descent() - font.lineSpacing();
int frameidx = frame % 32;
float framepos = (frameidx * (float)size[0]) / size[1];
float framesize = size[0] / (float)size[1];
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos+framesize, globalAlpha << 24 | 0xFFFFFF);
}));
}
public static RenderElement progressBars(SimpleFont font) {
return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame));
}
public static RenderElement performanceBar(SimpleFont font) {
return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.memoryInfo(font, bb, ctx, frame));
}
public static void startupProgressBars(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
Renderer acc = null;
var barCount = 2;
List<ProgressMeter> currentProgress = StartupNotificationManager.getCurrentProgress();
var size = currentProgress.size();
var alpha = 0xFF;
for (int i = 0; i < barCount && i < size; i++) {
final ProgressMeter pm = currentProgress.get(i);
Renderer barRenderer = barRenderer(i, alpha, font, pm, context);
acc = barRenderer.then(acc);
alpha >>= 1;
}
if (acc != null)
acc.accept(buffer, context, frameNumber);
}
private static final int BAR_HEIGHT = 20;
private static final int BAR_WIDTH = 400;
private static Renderer barRenderer(int cnt, int alpha, SimpleFont font, ProgressMeter pm, DisplayContext context) {
var barSpacing = font.lineSpacing() - font.descent() + BAR_HEIGHT;
var y = 250 * context.scale() + cnt * barSpacing;
var colour = (alpha << 24) | 0xFFFFFF;
Renderer bar;
if (pm.steps() == 0) {
bar = progressBar(ctx->new int[] {(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f->colour, frame -> indeterminateBar(frame, cnt == 0));
} else {
bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pm.progress()});
}
Renderer label = (bb, ctx, frame) -> renderText(font, text((ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, pm.label().getText(), colour), bb, ctx);
return bar.then(label);
}
private static float[] indeterminateBar(int frame, boolean isActive) {
if (RenderElement.globalAlpha != 0xFF || !isActive) {
return new float[] {0f,1f};
} else {
var progress = frame % 100;
return new float[]{clamp((progress - 2) / 100f, 0f, 1f), clamp((progress + 2) / 100f, 0f, 1f)};
}
}
private static void memoryInfo(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
var y = 10 * context.scale();
PerformanceInfo pi = context.performance();
final int colour = hsvToRGB((1.0f - (float)Math.pow(pi.memory(), 1.5f)) / 3f, 1.0f, 0.5f);
var bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pi.memory()});
var width = font.stringWidth(pi.text());
Renderer label = (bb, ctx, frame) -> renderText(font, text(ctx.scaledWidth() / 2 - width / 2, y + 18, pi.text(), context.colourScheme.foreground().packedint(globalAlpha)), bb, ctx);
bar.then(label).accept(buffer, context, frameNumber);
}
@FunctionalInterface
interface ColourFunction {
int colour(int frame);
}
@FunctionalInterface
interface ProgressDisplay {
float[] progress(int frame);
}
@FunctionalInterface
interface BarPosition {
int[] location(DisplayContext context);
}
public static Renderer progressBar(BarPosition position, ColourFunction colourFunction, ProgressDisplay progressDisplay) {
return (bb, context, frame) -> {
var colour = colourFunction.colour(frame);
var alpha = (colour & 0xFF000000) >> 24;
context.elementShader().updateTextureUniform(0);
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.BAR);
var progress = progressDisplay.progress(frame);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
var inset = 2;
var pos = position.location(context);
var x0 = pos[0];
var x1 = pos[0] + pos[2] + 4 * inset;
var y0 = pos[1];
var y1 = y0 + BAR_HEIGHT;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().foreground().packedint(alpha));
x0 += inset;
x1 -= inset;
y0 += inset;
y1 -= inset;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().background().packedint(RenderElement.globalAlpha));
x1 = x0 + inset + (int)(progress[1] * pos[2]);
x0 += inset + progress[0] * pos[2];
y0 += inset;
y1 -= inset;
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, colour);
bb.draw();
};
}
private static 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) {
context.elementShader().updateTextureUniform(font.textureNumber());
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.FONT);
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
textGenerator.accept(bb, font, context);
bb.draw();
}
private static TextGenerator text(int x, int y, String text, int colour) {
return (bb, font, context) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour));
}
private static Initializer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
return ()->{
int[] imgSize = STBHelper.loadTextureFromClasspath(textureFileName, size, GL_TEXTURE0 + textureNumber + INDEX_TEXTURE_OFFSET);
return (bb, ctx, frame) -> {
ctx.elementShader().updateTextureUniform(textureNumber + INDEX_TEXTURE_OFFSET);
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
renderTexture(bb, ctx, frame, imgSize, positionAndColour);
};
};
}
private static void renderTexture(SimpleBufferBuilder bb, DisplayContext context, int frame, int[] size, TextureRenderer positionAndColour) {
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
positionAndColour.accept(bb, context, size, frame);
bb.draw();
}
public static float clamp(float num, float min, float max) {
if (num < min) {
return min;
} else {
return Math.min(num, max);
}
}
public static int clamp(int num, int min, int max) {
if (num < min) {
return min;
} else {
return Math.min(num, max);
}
}
public static int hsvToRGB(float hue, float saturation, float value) {
int i = (int)(hue * 6.0F) % 6;
float f = hue * 6.0F - (float)i;
float f1 = value * (1.0F - saturation);
float f2 = value * (1.0F - f * saturation);
float f3 = value * (1.0F - (1.0F - f) * saturation);
float f4;
float f5;
float f6;
switch(i) {
case 0:
f4 = value;
f5 = f3;
f6 = f1;
break;
case 1:
f4 = f2;
f5 = value;
f6 = f1;
break;
case 2:
f4 = f1;
f5 = value;
f6 = f3;
break;
case 3:
f4 = f1;
f5 = f2;
f6 = value;
break;
case 4:
f4 = f3;
f5 = f1;
f6 = value;
break;
case 5:
f4 = value;
f5 = f1;
f6 = f2;
break;
default:
throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value);
}
int j = clamp((int)(f4 * 255.0F), 0, 255);
int k = clamp((int)(f5 * 255.0F), 0, 255);
int l = clamp((int)(f6 * 255.0F), 0, 255);
return 0xFF << 24 | j << 16 | k << 8 | l;
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.STBImage;
import org.lwjgl.system.MemoryUtil;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.util.Objects;
import static org.lwjgl.opengl.GL32C.*;
public class STBHelper {
public static ByteBuffer readFromClasspath(final String name, int initialCapacity) {
ByteBuffer buf;
try (var channel = Channels.newChannel(
Objects.requireNonNull(STBHelper.class.getClassLoader().getResourceAsStream(name), "The resource "+name+" cannot be found"))) {
buf = BufferUtils.createByteBuffer(initialCapacity);
while (true) {
var readbytes = channel.read(buf);
if (readbytes == -1) break;
if (buf.remaining() == 0) { // extend the buffer by 50%
var newBuf = BufferUtils.createByteBuffer(buf.capacity() * 3 / 2);
buf.flip();
newBuf.put(buf);
buf = newBuf;
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
buf.flip();
return MemoryUtil.memSlice(buf); // we trim the final buffer to the size of the content
}
public static int[] loadTextureFromClasspath(String file, int size, int textureNumber) {
int[] lw = new int[1];
int[] lh = new int[1];
int[] lc = new int[1];
var img = loadImageFromClasspath(file, size, lw, lh, lc);
var texid = glGenTextures();
glActiveTexture(textureNumber);
glBindTexture(GL_TEXTURE_2D, texid);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lw[0], lh[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, img);
glActiveTexture(GL_TEXTURE0);
MemoryUtil.memFree(img);
return new int[] {lw[0], lh[0]};
}
public static ByteBuffer loadImageFromClasspath(String file, int size, int[] width, int[] height, int[] channels) {
ByteBuffer buf = STBHelper.readFromClasspath(file, size);
return STBImage.stbi_load_from_memory(buf, width, height, channels, 4);
}
}
@@ -0,0 +1,517 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.system.MemoryUtil;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static org.lwjgl.opengl.GL32C.*;
/**
* A very simple, Mojang inspired BufferBuilder.
* <em>This has been customized for 2d rendering such as text and simple planar textures</em>
* <p>
* Not bound to any specific format, ideally should be held onto for re-use.
* <p>
* Can be used for 'immediate mode' style rendering using {@link #draw()}, or
* upload to external vertex arrays for proper instancing using {@link #finishAndUpload()}.
* <p>
* This is a Triangles only buffer, all data uploaded is in Triangles.
* Quads are converted to triangles using {@code 0, 1, 2, 0, 2, 3}.
* <p>
* Any given {@link Format} should have its individual {@link Element} components
* buffered in the order specified by the {@link Format},
* followed by an {@link #endVertex()} call to prepare for the next vertex.
* <p>
* It is illegal to buffer primitives in any format other than the one specified to
* {@link #begin(Format, Mode)}.
*
* @author covers1624
*/
public class SimpleBufferBuilder implements Closeable {
private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false);
private static final int[] VERTEX_ARRAYS = new int[Format.values().length];
private static final int[] VERTEX_BUFFERS = new int[Format.values().length];
private static final int[] VERTEX_BUFFER_LENGTHS = new int[Format.values().length];
private static int elementBuffer = 0;
private static int elementBufferVertexLength = 0;
static {
Arrays.fill(VERTEX_ARRAYS, 0);
Arrays.fill(VERTEX_BUFFERS, 0);
Arrays.fill(VERTEX_BUFFER_LENGTHS, 0);
}
private long bufferAddr; // Pointer to the backing buffer.
private ByteBuffer buffer; // ByteBuffer view of the backing buffer.
private Format format; // The current format we are buffering.
private Mode mode; // The current mode we are buffering.
private boolean building; // If we are building the buffer.
private int elementIndex; // The current element index we are buffering. if elementIndex == format.types.length, we expect 'endVertex'
private int index; // The current index into the buffer we are writing to.
private int vertices; // The number of complete vertices we have buffered.
/**
* Create a new SimpleBufferBuilder with an initial capacity.
* <p>
* The buffer will be doubled as required.
* <p>
* Generally picking a small number, around 128/256 should be a
* safe bet. Provided you cache your buffers, it should not mean much overall.
*
* @param capacity The initial capacity in bytes.
*/
public SimpleBufferBuilder(int capacity) {
bufferAddr = ALLOCATOR.malloc(capacity);
buffer = MemoryUtil.memByteBuffer(bufferAddr, capacity);
}
public static void destroy() {
glDeleteBuffers(VERTEX_BUFFERS);
glDeleteBuffers(elementBuffer);
glDeleteVertexArrays(VERTEX_ARRAYS);
}
private static void ensureElementBufferLength(int vertices) {
if (elementBufferVertexLength >= vertices) {
return;
}
// treating it as immutable storage, even though it's not
final var newElementBuffer = glGenBuffers();
var newElementBufferVertexLength = Math.max(1024, elementBufferVertexLength);
while (newElementBufferVertexLength < vertices) {
newElementBufferVertexLength *= 2;
}
final var oldIndexCount = elementBufferVertexLength + elementBufferVertexLength / 2;
final var newIndexCount = newElementBufferVertexLength + newElementBufferVertexLength / 2;
// allocate new buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, newElementBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, newIndexCount * 4L, GL_STATIC_DRAW);
// mapping avoids creating additional CPU copies of the data
// unsynchronized is fine because this is a brand-new buffer, and the old contents will be copied in afterward
// also can invalidate the whole buffer too, similarly because brand new, don't care what was there before
final var mappingOffset = oldIndexCount * 4;
final var mappingSize = (newIndexCount - oldIndexCount) * 4;
final var mappedBuffer = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, mappingOffset, mappingSize, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
if(mappedBuffer == null){
throw new NullPointerException("OpenGL buffer mapping failed");
}
final int quads = newElementBufferVertexLength / 4;
final int oldQuads = elementBufferVertexLength / 4;
// generate indices for the extension to the buffer
for (int i = oldQuads; i < quads; i++) {
// Quads are a bit different, we need to emit 2 triangles such that
// when combined they make up a single quad.
mappedBuffer.putInt(i * 4 + 0).putInt(i * 4 + 1).putInt(i * 4 + 2);
mappedBuffer.putInt(i * 4 + 1).putInt(i * 4 + 3).putInt(i * 4 + 2);
}
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
if (elementBuffer != 0) {
// copy old data from previous element buffer
glBindBuffer(GL_COPY_READ_BUFFER, elementBuffer);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, mappingOffset);
glBindBuffer(GL_COPY_READ_BUFFER, 0);
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glDeleteBuffers(elementBuffer);
elementBuffer = newElementBuffer;
elementBufferVertexLength = newElementBufferVertexLength;
}
/**
* Start building a new set of vertex data in the
* given format and mode.
*
* @param format The format to start building in.
* @param mode The mode to start building in.
*/
public SimpleBufferBuilder begin(Format format, Mode mode) {
if (bufferAddr == MemoryUtil.NULL) {
throw new IllegalStateException("Buffer has been freed."); // You already free'd the buffer
}
if (building) {
throw new IllegalStateException("Already building."); // Your already building verticies.
}
this.format = format;
this.mode = mode;
building = true;
elementIndex = 0;
ensureSpace(format.stride);
// Rewind ready for new data.
buffer.rewind();
buffer.limit(buffer.capacity());
return this;
}
/**
* Buffer a position element.
*
* @param x The x.
* @param y The y.
* @param z The z.
* @return The same builder.
*/
public SimpleBufferBuilder pos(float x, float y) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.POS) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes that our POS element specifies the FLOAT data type.
buffer.putFloat(index + 0, x);
buffer.putFloat(index + 4, y);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a texture element.
*
* @param u The u.
* @param v The v.
* @return The same builder.
*/
public SimpleBufferBuilder tex(float u, float v) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.TEX) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our TEX element specifies the FLOAT data type.
buffer.putFloat(index + 0, u);
buffer.putFloat(index + 4, v);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a color element.
*
* @param r The red component. (0-1)
* @param g The green component. (0-1)
* @param b The blue component. (0-1)
* @param a The alpha component. (0-1)
* @return The same buffer.
*/
public SimpleBufferBuilder colour(float r, float g, float b, float a) {
// Expand floats to 0-255 and forward.
return colour((byte) (r * 255F), (byte) (g * 255F), (byte) (b * 255F), (byte) (a * 255F));
}
/**
* @see ColourScheme.Colour#packedint(int)
* @param packedColor an ABGR packed int
* @return the same buffer.
*/
public SimpleBufferBuilder colour(int packedColor) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
buffer.putInt(index + 0, packedColor);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* Buffer a color element.
*
* @param r The red component. (0-255)
* @param g The green component. (0-255)
* @param b The blue component. (0-255)
* @param a The alpha component. (0-255)
* @return The same buffer.
*/
public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
buffer.put(index + 0, r);
buffer.put(index + 1, g);
buffer.put(index + 2, b);
buffer.put(index + 3, a);
// Increment index for the number of bytes we wrote and increment the element index.
index += format.types[elementIndex].width;
elementIndex++;
return this;
}
/**
* End building the current vertex and prepare for the next.
*
* @return The same builder.
*/
public SimpleBufferBuilder endVertex() {
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
if (elementIndex != format.types.length) throw new IllegalStateException("Expected " + format.types[elementIndex]); // You did not finish building the vertex.
// Reset elementIndex
elementIndex = 0;
// Increment the number of vertices we have so far buffered.
vertices++;
// Make sure there is space for the next vertex.
ensureSpace(format.stride);
return this;
}
// Checks there is enough space in the buffer for specified number of bytes.
// If there is not enough space, the buffer is increased by 50%.
private void ensureSpace(int newBytes) {
int cap = buffer.capacity();
if (index + newBytes > cap) {
int newCap = Math.max(3 * cap / 2, 3 * newBytes / 2);
bufferAddr = ALLOCATOR.realloc(bufferAddr, newCap);
buffer = MemoryUtil.memByteBuffer(bufferAddr, newCap);
buffer.rewind();
}
}
/**
* Upload the current buffer.
* <p>
* This will bind a {@link org.lwjgl.opengl.GL32C#GL_ARRAY_BUFFER} and {@link org.lwjgl.opengl.GL32C#GL_ELEMENT_ARRAY_BUFFER}
* <p>
* The vertex data and index data is uploaded to their respective buffers.
* <p>
* Uploading the buffers finishes drawing and resets for the next buffer operation.
* <p>
* This should not be called in conjunction with {@link #draw()}
*
* @return The number of indexes that were uploaded.
*/
public int finishAndUpload() {
if (!building) throw new IllegalStateException("Not building.");
int indices;
try {
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex.
if (elementIndex != 0) throw new IllegalStateException("Not finished building vertex, Expected: " + format.types[elementIndex]); // You didn't finish building your vertex data.
if (vertices == 0) return 0; // No vertices buffered, lets not do anything.
if (vertices % mode.vertices != 0) throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS
// Reset position to 0, limit the buffer to our index.
buffer.position(0);
buffer.limit(index);
// Upload the raw vertex data in dynamic mode.
final int vbo = VERTEX_BUFFERS[format.ordinal()];
final int vboSize = VERTEX_BUFFER_LENGTHS[format.ordinal()];
glBindBuffer(GL_ARRAY_BUFFER, vbo);
if (vboSize < index) {
// expand buffer, it's not big enough
var newVBOSize = Math.max(1024, vboSize);
while (newVBOSize < index){
newVBOSize *= 2;
}
// because everything is overwritten anyway, we can do an in-place reallocation
glBufferData(GL_ARRAY_BUFFER, newVBOSize, GL_DYNAMIC_DRAW);
VERTEX_BUFFER_LENGTHS[format.ordinal()] = newVBOSize;
}
glBufferSubData(GL_ARRAY_BUFFER, 0, buffer);
// The number of indices for triangles is equal to our vertex count, as that is
// what we operate in. However, for Quads, we have exactly vertices + vertices / 2
// vertices once we convert the quads to triangles.
indices = mode == Mode.TRIANGLES ? vertices : vertices + vertices / 2;
if (mode == Mode.QUADS) {
ensureElementBufferLength(vertices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
}
return indices;
} finally {
// Reset builder state for next begin call.
building = false;
vertices = 0;
index = 0;
}
}
/**
* Upload and draw this buffer using one of a number of re-usable set of buffers.
* <p>
* This will immediately upload the buffer, resetting this builder for the next
* buffer operation, and draw the uploaded data.
* <p>
* You will need to bind shaders, textures, etc, before calling this function.
*/
public void draw() {
if (!building) throw new IllegalStateException("Not building.");
int vao = VERTEX_ARRAYS[format.ordinal()];
int vbo = VERTEX_BUFFERS[format.ordinal()];
if (vao == 0) {
// These 3 buffers are paired, you can't allocate one without the others.
assert vbo == 0;
// Make new vertex array and buffers!
vao = glGenVertexArrays();
vbo = glGenBuffers();
// Cache the vertex array and buffers for future re-use.
VERTEX_ARRAYS[format.ordinal()] = vao;
VERTEX_BUFFERS[format.ordinal()] = vbo;
// Ask our Format to set up its data layout for the vertex array.
// but only once, the VAO saves this state
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
format.bind();
format.enable();
}
// Bind the vertex array and buffers!
glBindVertexArray(vao);
// Upload the data.
int indices = finishAndUpload();
if (mode == Mode.QUADS) {
glDrawElements(GL_TRIANGLES, indices, GL_UNSIGNED_INT, 0);
} else {
glDrawArrays(GL_TRIANGLES, 0, indices);
}
// Unbind the vertex array.
glBindVertexArray(0);
}
/**
* Clear this builder's cached buffer.
* <p>
* If you are completely done, call {@link #destroy()}
*/
@Override
public void close() {
ALLOCATOR.free(bufferAddr);
bufferAddr = MemoryUtil.NULL;
}
/**
* Represents a primitive mode that this builder is capable of buffering in.
*/
public enum Mode {
TRIANGLES(3),
QUADS(4),
;
public final int vertices;
Mode(int vertices) {
this.vertices = vertices;
}
}
/**
* Specifies a vertex element with a specific data type, number of primitives and a size in bytes.
*/
public enum Element {
POS(GL_FLOAT, 2, 2 * 4),
TEX(GL_FLOAT, 2, 2 * 4),
COLOR(GL_UNSIGNED_BYTE, 4, 4);
public final int glType;
public final int count;
public final int width;
Element(int glType, int count, int width) {
this.glType = glType;
this.count = count;
this.width = width;
}
}
/**
* Specifies a combination of vertex elements.
*/
public enum Format {
POS(Element.POS),
POS_TEX(Element.POS, Element.TEX),
POS_COLOR(Element.POS, Element.COLOR),
POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR);
private final Element[] types;
public final int stride;
Format(Element... types) {
this.types = types;
// Stride is the width of each vertex in bytes.
stride = Arrays.stream(types).mapToInt(e -> e.width).sum();
}
/**
* set up the attribute pointers for this format.
* <p>
* Assumes that an array buffer is already bound and ready to go.
*/
public void bind() {
int offset = 0;
// Set up the pointers that tell GL where our interleaved
// vertex data is in the buffers.
for (int i = 0; i < types.length; i++) {
Element type = types[i];
switch (type.glType) {
case GL_FLOAT -> glVertexAttribPointer(i, type.count, GL_FLOAT, false, stride, offset);
case GL_UNSIGNED_BYTE -> glVertexAttribPointer(i, type.count, GL_UNSIGNED_BYTE, true, stride, offset);
default -> throw new IllegalStateException("Unknown glType, I don't know how to bind this vertex element: " + type);
}
// add to the offset for the next element.
offset += type.width;
}
}
/**
* Enables the vertex attributes this format contains.
*/
public void enable() {
for (int i = 0; i < types.length; i++) {
glEnableVertexAttribArray(i);
}
}
/**
* Disables the vertex attributes this format contains.
*/
public void disable() {
for (int i = 0; i < types.length; i++) {
glDisableVertexAttribArray(i);
}
}
}
}
@@ -0,0 +1,180 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.earlydisplay;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.STBTTAlignedQuad;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTTPackContext;
import org.lwjgl.stb.STBTTPackRange;
import org.lwjgl.stb.STBTTPackedchar;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static org.lwjgl.opengl.GL32C.*;
import static org.lwjgl.stb.STBTruetype.*;
import static org.lwjgl.system.MemoryUtil.NULL;
public class SimpleFont {
private final int textureNumber;
private final int lineSpacing;
private final int descent;
private final int GLYPH_COUNT = 127-32;
private final Glyph[] glyphs;
private record Glyph(char c, int charwidth, int[] pos, float[] uv) {
Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) {
final var x0 = pos.x() + pos()[0];
final var y0 = pos.y() + pos()[1];
final var x1 = pos.x() + pos()[2];
final var y1 = pos.y() + pos()[3];
bb.pos(x0, y0).tex(uv()[0], uv()[1]).colour(colour).endVertex();
bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex();
bb.pos(x0, y1).tex(uv()[0], uv()[3]).colour(colour).endVertex();
bb.pos(x1, y1).tex(uv()[2], uv()[3]).colour(colour).endVertex();
return new Pos(pos.x()+charwidth(), pos.y(), pos.minx());
}
}
/**
* Build the font and store it in the textureNumber location
*/
public SimpleFont(String fontName, int scale, int bufferSize, int textureNumber) {
ByteBuffer buf = STBHelper.readFromClasspath(fontName, bufferSize);
var info = STBTTFontinfo.create();
if (!stbtt_InitFont(info, buf)) {
throw new IllegalStateException("Bad font");
}
var ascent = new float[1];
var descent = new float[1];
var lineGap = new float[1];
int fontSize = 24;
stbtt_GetScaledFontVMetrics(buf, 0, fontSize, ascent, descent, lineGap);
this.lineSpacing = (int)(ascent[0] - descent[0] + lineGap[0]);
this.descent = (int)Math.floor(descent[0]);
int fontTextureId = glGenTextures();
glActiveTexture(GL_TEXTURE0+textureNumber);
this.textureNumber = textureNumber;
glBindTexture(GL_TEXTURE_2D, fontTextureId);
try (var packedchars = STBTTPackedchar.malloc(GLYPH_COUNT)) {
int texwidth = 256;
int texheight = 128;
try (STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1)) {
var bitmap = BufferUtils.createByteBuffer(texwidth * texheight);
try (STBTTPackRange packRange = STBTTPackRange.malloc()) {
packRanges.put(packRange.set(fontSize, 32, null, GLYPH_COUNT, packedchars, (byte) 1, (byte) 1));
packRanges.flip();
}
try (STBTTPackContext pc = STBTTPackContext.malloc()) {
stbtt_PackBegin(pc, bitmap, texwidth, texheight, 0, 1, NULL);
stbtt_PackSetOversampling(pc, 1, 1);
stbtt_PackSetSkipMissingCodepoints(pc, true);
stbtt_PackFontRanges(pc, buf, 0, packRanges);
stbtt_PackEnd(pc);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, texwidth, texheight, 0, GL_RED, GL_UNSIGNED_BYTE, bitmap);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
}
glActiveTexture(GL_TEXTURE0);
try (var q = STBTTAlignedQuad.malloc()) {
float[] x = new float[1];
float[] y = new float[1];
glyphs = new Glyph[GLYPH_COUNT];
for (int i = 0; i < GLYPH_COUNT; i++) {
x[0] = 0f;
y[0] = fontSize;
stbtt_GetPackedQuad(packedchars, texwidth, texheight, i, x, y, q, true);
glyphs[i] = new Glyph((char) (i + 32), (int) (x[0] - 0f), new int[]{(int) q.x0(), (int) q.y0(), (int) q.x1(), (int) q.y1()}, new float[]{q.s0(), q.t0(), q.s1(), q.t1()});
}
}
}
}
int lineSpacing() {
return lineSpacing;
}
int textureNumber() {
return textureNumber;
}
int descent() {
return descent;
}
public int stringWidth(String text) {
var bytes = text.getBytes(StandardCharsets.US_ASCII);
int len = 0;
for (int i = 0; i < bytes.length; i++) {
final byte c = bytes[i];
len += switch (c) {
case '\n', '\t' -> 0;
case ' ' -> glyphs[0].charwidth();
default -> {
if (c - 32 < this.GLYPH_COUNT && c > 32) {
yield this.glyphs[c - 32].charwidth();
} else {
yield 0;
}
}
};
}
return len;
}
private record Pos(int x, int y, int minx) {}
/**
* A piece of text to display
*
* @param string The text
* @param colour The colour of the text as an RGBA packed int
*/
public record DisplayText(String string, int colour) {
private byte[] asBytes() {
return string.getBytes(StandardCharsets.US_ASCII);
}
Pos generateStringArray(SimpleFont font, Pos pos, SimpleBufferBuilder bb) {
for (int i = 0; i < asBytes().length; i++) {
byte c = asBytes()[i];
pos = switch (c) {
case '\n' -> new Pos(pos.minx(), pos.y()+font.lineSpacing(), pos.minx());
case '\t' -> new Pos(pos.x()+font.glyphs[0].charwidth() * 4, pos.y(), pos.minx());
case ' ' -> new Pos(pos.x()+font.glyphs[0].charwidth(), pos.y(), pos.minx());
default -> {
if (c-32 < font.GLYPH_COUNT && c > 32) {
pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb);
}
yield pos;
}
};
}
return pos;
}
}
/**
* Generate vertices for a set of display texts
* @param x The starting screen x coordinate
* @param y The starting screen y coordinate
* @param texts Some {@link DisplayText} to display
* @return a {@link SimpleBufferBuilder} that can draw the texts
*/
public SimpleBufferBuilder generateVerticesForTexts(int x, int y, SimpleBufferBuilder textBB, DisplayText... texts) {
var pos = new Pos(x, y, x);
for (DisplayText text : texts) {
pos = text.generateStringArray(this, pos, textBB);
}
return textBB;
}
}
@@ -0,0 +1 @@
net.minecraftforge.fml.earlydisplay.DisplayWindow
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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

After

Width:  |  Height:  |  Size: 43 KiB

+82 -71
View File
@@ -1,95 +1,106 @@
plugins {
id 'com.github.ben-manes.versions'
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
id 'org.cadixdev.licenser'
}
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply plugin: 'org.javamodularity.moduleplugin'
import net.minecraftforge.gradleutils.PomUtils
plugins {
id 'java-library'
id 'maven-publish'
id 'net.minecraftforge.licenser'
id 'net.minecraftforge.gradleutils'
}
apply from: rootProject.file('build_shared.gradle')
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
configurations.forEach{ it.transitive = false }
dependencies {
compileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
compileOnly(libs.nulls)
api("org.ow2.asm:asm:${ASM_VERSION}")
api("org.ow2.asm:asm-tree:${ASM_VERSION}")
api("org.ow2.asm:asm-commons:${ASM_VERSION}")
api("net.minecraftforge:forgespi:${SPI_VERSION}")
api("org.apache.logging.log4j:log4j-api:${LOG4J_VERSION}")
api("org.slf4j:slf4j-api:${SLF4J_API_VERSION}")
api("com.google.guava:guava:${GUAVA_VERSION}")
api("com.google.code.gson:gson:${GSON_VERSION}")
api("org.apache.maven:maven-artifact:${APACHE_MAVEN_ARTIFACT_VERSION}")
api("org.apache.commons:commons-lang3:${APACHE_COMMONS_LANG3_VERSION}")
api("com.electronwill.night-config:core:${NIGHTCONFIG_VERSION}")
api("com.electronwill.night-config:toml:${NIGHTCONFIG_VERSION}")
api("cpw.mods:modlauncher:${MODLAUNCHER_VERSION}")
api("net.minecraftforge:coremods:${COREMODS_VERSION}")
api("com.mojang:logging:${MOJANG_LOGGING_VERSION}")
api "net.minecraftforge:JarJarSelector:${JARJAR_VERSION}"
api "net.minecraftforge:JarJarMetadata:${JARJAR_VERSION}"
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("net.sf.jopt-simple:jopt-simple:${JOPT_SIMPLE_VERSION}")
implementation("cpw.mods:securejarhandler:${SECUREJARHANDLER_VERSION}")
implementation("net.minecraftforge:accesstransformers:${ACCESSTRANSFORMERS_VERSION}")
implementation("net.minecrell:terminalconsoleappender:${TERMINALCONSOLEAPPENDER_VERSION}")
// implementation("org.jline:jline:${JLINE_VERSION}")
implementation(libs.jopt.simple)
implementation(libs.securemodules)
implementation(libs.accesstransformers)
implementation(libs.terminalconsoleappender)
implementation(libs.jimfs)
implementation("org.apache.logging.log4j:log4j-core:${LOG4J_VERSION}")
annotationProcessor("org.apache.logging.log4j:log4j-core:${LOG4J_VERSION}")
implementation(libs.log4j.core)
annotationProcessor(libs.bundles.log4j)
testCompileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
testRuntimeOnly("cpw.mods:bootstraplauncher:${BOOTSTRAPLAUNCHER_VERSION}")
testCompileOnly(libs.nulls)
testRuntimeOnly(libs.bootstrap)
}
task sourcesJar(type: Jar) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
java {
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
withSourcesJar()
}
ext {
MANIFESTS = [
'/': [
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
'Git-Commit': GIT_INFO.abbreviatedId,
'Git-Branch': GIT_INFO.branch,
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}",
] as LinkedHashMap,
'net/minecraftforge/fml/loading/': [
'Specification-Title': 'FMLLoader',
'Specification-Vendor': 'Forge Development LLC',
'Specification-Version': '1',
'Implementation-Title': 'FML Loader',
'Implementation-Version': '1.0',
'Implementation-Vendor': 'Forge'
] as LinkedHashMap
]
}
jar.doFirst {
MANIFESTS.each { pkg, values ->
manifest.attributes(values, pkg)
tasks.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/')
}
}
tasks.withType(JavaCompile) {
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-Xlint:unchecked'
}
license {
header = rootProject.file('LICENSE-header.txt')
include 'net/minecraftforge/'
}
artifacts {
archives jar
archives sourcesJar
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
}
}
publishing.publications.mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
}
tasks.register('writeForgeVersionJson') {
doLast {
file('src/main/resources/forge_version.json').json = [
forge: FORGE_VERSION,
mc: MC_VERSION,
mcp: MCP_VERSION
]
}
}
tasks.named('generateResources').configure {
dependsOn('writeForgeVersionJson')
}
@@ -36,9 +36,9 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
*/
public class CapabilityTokenSubclass implements ILaunchPluginService {
private final String FUNC_NAME = "getType";
private final String FUNC_DESC = "()Ljava/lang/String;";
private final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; //Don't directly reference this to prevent class loading.
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.
@Override
public String name() {
@@ -49,49 +49,45 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraft/") || internalName.startsWith("com/mojang/"))
return NAY;
return YAY;
}
@Override
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason)
{
if (CAP_INJECT.equals(classNode.name))
{
for (MethodNode mtd : classNode.methods)
{
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc))
{
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason) {
if (CAP_INJECT.equals(classNode.name)) {
for (MethodNode mtd : classNode.methods) {
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc)) {
mtd.access &= ~Opcodes.ACC_FINAL; // We have it final in code so people don't override it, cuz that'd be stupid, and make our transformer more complicated.
}
}
return ComputeFlags.SIMPLE_REWRITE;
}
else if (CAP_INJECT.equals(classNode.superName))
{
} else if (CAP_INJECT.equals(classNode.superName)) {
Holder cls = new Holder();
SignatureReader reader = new SignatureReader(classNode.signature); // Having a node version of this would probably be useful.
reader.accept(new SignatureVisitor(Opcodes.ASM9)
{
Deque<String> stack = new ArrayDeque<>();
reader.accept(new SignatureVisitor(Opcodes.ASM9) {
final Deque<String> stack = new ArrayDeque<>();
@Override
public void visitClassType(final String name)
{
public void visitClassType(final String name) {
stack.push(name);
}
@Override
public void visitInnerClassType(final String name)
{
public void visitInnerClassType(final String name) {
stack.push(stack.pop() + '$' + name);
}
@Override
public void visitEnd()
{
public void visitEnd() {
var val = stack.pop();
if (!stack.isEmpty() && CAP_INJECT.equals(stack.peek()))
cls.value = val;
@@ -106,15 +102,12 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
mtd.visitInsn(Opcodes.ARETURN);
mtd.visitEnd();
return ComputeFlags.COMPUTE_MAXS;
}
else
{
} else {
return ComputeFlags.NO_REWRITE;
}
}
private static class Holder {
private static final class Holder {
String value;
}
}
@@ -8,6 +8,7 @@ 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;
@@ -34,8 +35,12 @@ public class ObjectHolderDefinalize implements ILaunchPluginService {
new VanillaObjectHolderData("net.minecraft.world.effect.MobEffects", "mob_effect", "net.minecraft.world.effect.MobEffect"),
new VanillaObjectHolderData("net.minecraft.core.particles.ParticleTypes", "particle_type", "net.minecraft.core.particles.ParticleType"),
new VanillaObjectHolderData("net.minecraft.sounds.SoundEvents", "sound_event", "net.minecraft.sounds.SoundEvent")
).collect(Collectors.toMap(VanillaObjectHolderData::holderClass, Function.identity()));
private final String OBJECT_HOLDER = "Lnet/minecraftforge/registries/ObjectHolder;"; //Don't directly reference this to prevent class loading.
).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() {
@@ -46,16 +51,33 @@ public class ObjectHolderDefinalize implements ILaunchPluginService {
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty)
return NAY;
// Let's skip processing classes that definitely don't have object holders
String internalName = classType.getInternalName();
// Forge classes that aren't in the debug package (that package is used for tests)
if (internalName.startsWith("net/minecraftforge/") && !internalName.substring(18).contains("debug"))
return NAY;
// Vanilla classes that don't have object holders
if (internalName.startsWith("com/mojang/"))
return NAY;
// ...except specific ones we added to the VANILLA_OBJECT_HOLDERS map
if (internalName.startsWith("net/minecraft/") && !VANILLA_OBJECT_HOLDER_CLASSES.contains(internalName))
return NAY;
return YAY;
}
private boolean hasHolder(List<AnnotationNode> lst)
{
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();
@@ -75,10 +97,9 @@ public class ObjectHolderDefinalize implements ILaunchPluginService {
{
final AtomicBoolean changes = new AtomicBoolean();
//Must be public static finals, and non-array objects
final int flags = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL;
//Fix Annotated Fields before injecting from class level
classNode.fields.stream().filter(f -> ((f.access & flags) == flags) && f.desc.startsWith("L") && hasHolder(f.visibleAnnotations)).forEach(f ->
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
@@ -88,7 +109,7 @@ public class ObjectHolderDefinalize implements ILaunchPluginService {
if (VANILLA_OBJECT_HOLDERS.containsKey(classType.getClassName())) //Class level, de-finalize all fields and add @ObjectHolder to them!
{
classNode.fields.stream().filter(f -> ((f.access & flags) == flags) && f.desc.startsWith("L")).forEach(f ->
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;
@@ -20,8 +20,6 @@ import org.objectweb.asm.tree.MethodNode;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
/**
* Modifies specified enums to allow runtime extension by making the $VALUES field non-final and
@@ -30,15 +28,16 @@ import org.slf4j.MarkerFactory;
public class RuntimeEnumExtender implements ILaunchPluginService {
private static final Logger LOGGER = LogUtils.getLogger();
private final Type STRING = Type.getType(String.class);
private final Type ENUM = Type.getType(Enum.class);
private final Type MARKER_IFACE = Type.getType("Lnet/minecraftforge/common/IExtensibleEnum;");
private final Type ARRAY_UTILS = Type.getType("Lorg/apache/commons/lang3/ArrayUtils;"); //Don't directly reference this to prevent class loading.
private final String ADD_DESC = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(Object[].class), Type.getType(Object.class));
private final Type UNSAFE_HACKS = Type.getType("Lnet/minecraftforge/fml/unsafe/UnsafeHacks;"); //Again, not direct reference to prevent class loading.
private final String CLEAN_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Class.class));
private final String NAME_DESC = Type.getMethodDescriptor(STRING);
private final String EQUALS_DESC = Type.getMethodDescriptor(Type.BOOLEAN_TYPE, STRING);
private static final Type STRING = Type.getType(String.class);
private static final Type ENUM = Type.getType(Enum.class);
private static final Type MARKER_IFACE = Type.getType("Lnet/minecraftforge/common/IExtensibleEnum;");
private static final Type ARRAY_UTILS = Type.getType("Lorg/apache/commons/lang3/ArrayUtils;"); //Don't directly reference this to prevent class loading.
private static final String ADD_DESC = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(Object[].class), Type.getType(Object.class));
private static final Type UNSAFE_HACKS = Type.getType("Lnet/minecraftforge/fml/unsafe/UnsafeHacks;"); //Again, not direct reference to prevent class loading.
private static final String CLEAN_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Class.class));
private static final String NAME_DESC = Type.getMethodDescriptor(STRING);
private static final String EQUALS_DESC = Type.getMethodDescriptor(Type.BOOLEAN_TYPE, STRING);
private static final int FLAGS = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC;
@Override
public String name() {
@@ -49,9 +48,15 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
{
return isEmpty ? NAY : YAY;
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
if (isEmpty)
return NAY;
String internalName = classType.getInternalName();
if (internalName.startsWith("net/minecraftforge/") || internalName.startsWith("com/mojang/"))
return NAY;
return YAY;
}
@Override
@@ -60,25 +65,24 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
if ((classNode.access & Opcodes.ACC_ENUM) == 0)
return ComputeFlags.NO_REWRITE;
Type array = Type.getType("[" + classType.getDescriptor());
final int flags = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC;
FieldNode values = classNode.fields.stream().filter(f -> f.desc.contentEquals(array.getDescriptor()) && ((f.access & flags) == flags)).findFirst().orElse(null);
if (!classNode.interfaces.contains(MARKER_IFACE.getInternalName())) {
if (!classNode.interfaces.contains(MARKER_IFACE.getInternalName()))
return ComputeFlags.NO_REWRITE;
}
Type array = Type.getType("[" + classType.getDescriptor());
String arrayDesc = array.getDescriptor();
FieldNode values = classNode.fields.stream().filter(f -> f.desc.equals(arrayDesc) && ((f.access & FLAGS) == FLAGS)).findFirst().orElse(null);
//Static methods named "create" with first argument as a string
List<MethodNode> candidates = classNode.methods.stream()
.filter(m -> ((m.access & Opcodes.ACC_STATIC) != 0) && m.name.equals("create"))
.collect(Collectors.toList());
.toList();
if (candidates.isEmpty()) {
throw new IllegalStateException("IExtensibleEnum has no candidate factory methods: " + classType.getClassName());
}
candidates.forEach(mtd ->
for (var mtd : candidates)
{
Type[] args = Type.getArgumentTypes(mtd.desc);
if (args.length == 0 || !args[0].equals(STRING)) {
@@ -110,8 +114,7 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
Type[] ctrArgs = new Type[args.length + 1];
ctrArgs[0] = STRING;
ctrArgs[1] = Type.INT_TYPE;
for (int x = 1; x < args.length; x++)
ctrArgs[1 + x] = args[x];
System.arraycopy(args, 1, ctrArgs, 2, args.length - 1);
String desc = Type.getMethodDescriptor(Type.VOID_TYPE, ctrArgs);
@@ -233,7 +236,7 @@ public class RuntimeEnumExtender implements ILaunchPluginService {
ins.load(vars, classType);
ins.areturn(classType);
}
});
}
return ComputeFlags.COMPUTE_FRAMES;
}
@@ -11,9 +11,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class BackgroundWaiter {
private static ExecutorService runner = Executors.newSingleThreadExecutor();
private static final ExecutorService runner = Executors.newSingleThreadExecutor();
public static void runAndTick(Runnable r, Runnable tick) {
ImmediateWindowHandler.updateProgress("Loading bootstrap resources");
final Future<?> work = runner.submit(r);
do {
tick.run();
@@ -1,61 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading;
import cpw.mods.modlauncher.api.NamedPath;
import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService;
import org.apache.logging.log4j.LogManager;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class ClasspathTransformerDiscoverer implements ITransformerDiscoveryService {
private final List<Path> legacyClasspath = Arrays.stream(System.getProperty("legacyClassPath", "").split(File.pathSeparator)).map(Path::of).toList();
@Override
public List<NamedPath> candidates(Path gameDirectory) {
return Collections.emptyList();
}
@Override
public List<NamedPath> candidates(final Path gameDirectory, final String launchTarget) {
if (launchTarget != null && launchTarget.contains("dev")) {
this.scan(gameDirectory);
}
return List.copyOf(found);
}
private final static List<NamedPath> found = new ArrayList<>();
public static List<Path> allExcluded() {
return found.stream().map(np->np.paths()[0]).toList();
}
private void scan(final Path gameDirectory) {
try {
locateTransformers("META-INF/services/cpw.mods.modlauncher.api.ITransformationService");
locateTransformers("META-INF/services/net.minecraftforge.forgespi.locating.IModLocator");
} catch (IOException e) {
LogManager.getLogger().error("Error during discovery of transform services from the classpath", e);
}
}
private void locateTransformers(String resource) throws IOException {
final Enumeration<URL> resources = ClassLoader.getSystemClassLoader().getResources(resource);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
Path path = ClasspathLocatorUtils.findJarPathFor(resource, url.toString(), url);
if (legacyClasspath.stream().anyMatch(path::equals) || !Files.exists(path) || Files.isDirectory(path))
continue;
found.add(new NamedPath(path.toUri().toString(), path));
}
}
}
@@ -14,6 +14,8 @@ import java.util.List;
* or server.
*/
public class EarlyLoadingException extends RuntimeException {
private static final long serialVersionUID = 2302192216172508006L;
public static class ExceptionData {
private final IModInfo modInfo;
private final String i18message;
@@ -5,6 +5,7 @@
package net.minecraftforge.fml.loading;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.ConfigSpec;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
@@ -15,21 +16,79 @@ import org.slf4j.Logger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
public class FMLConfig
{
public enum ConfigValue {
EARLY_WINDOW_CONTROL("earlyWindowControl", Boolean.TRUE, "Should we control the window. Disabling this disables new GL features and can be bad for mods that rely on them."),
MAX_THREADS("maxThreads", -1, "Max threads for early initialization parallelism, -1 is based on processor count", FMLConfig::maxThreads),
VERSION_CHECK("versionCheck", Boolean.TRUE, "Enable forge global version checking"),
DEFAULT_CONFIG_PATH("defaultConfigPath", "defaultconfigs", "Default config path for servers"),
DISABLE_OPTIMIZED_DFU("disableOptimizedDFU", Boolean.TRUE, "Disables Optimized DFU client-side - already disabled on servers"),
EARLY_WINDOW_PROVIDER("earlyWindowProvider", "fmlearlywindow", "Early window provider"),
EARLY_WINDOW_WIDTH("earlyWindowWidth", 854, "Early window width"),
EARLY_WINDOW_HEIGHT("earlyWindowHeight", 480, "Early window height"),
EARLY_WINDOW_FBSCALE("earlyWindowFBScale", 1, "Early window framebuffer scale"),
EARLY_WINDOW_MAXIMIZED("earlyWindowMaximized", Boolean.FALSE, "Early window starts maximized"),
EARLY_WINDOW_SKIP_GL_VERSIONS("earlyWindowSkipGLVersions", List.of(), "Skip specific GL versions, may help with buggy graphics card drivers"),
EARLY_WINDOW_SQUIR("earlyWindowSquir", Boolean.FALSE, "Squir?"),
EARLY_WINDOW_SHOW_CPU("earlyWindowShowCPU", Boolean.FALSE, "Whether to show CPU usage stats in early window"),
EARLY_WINDOW_LOG_HELP_MSG("earlyWindowLogHelpMessage", Boolean.TRUE, "Whether to log a help message on first attempt, to aid troubleshooting. This setting should automatically disable itself after a successful launch"),
;
private final String entry;
private final Object defaultValue;
private final String comment;
private final Function<Object, Object> entryFunction;
ConfigValue(final String entry, final Object defaultValue, final String comment) {
this(entry, defaultValue, comment, Function.identity());
}
ConfigValue(final String entry, final Object defaultValue, final String comment, Function<Object, Object> entryFunction) {
this.entry = entry;
this.defaultValue = defaultValue;
this.comment = comment;
this.entryFunction = entryFunction;
}
void buildConfigEntry(ConfigSpec spec, CommentedConfig commentedConfig) {
if (this.defaultValue instanceof List<?> list) {
spec.defineList(this.entry, list, e -> e instanceof String);
} else {
spec.define(this.entry, this.defaultValue);
}
commentedConfig.add(this.entry, this.defaultValue);
commentedConfig.setComment(this.entry, this.comment);
}
@SuppressWarnings("unchecked")
private <T> T getConfigValue(CommentedFileConfig config) {
return (T) this.entryFunction.apply(config.get(this.entry));
}
public <T> void updateValue(final CommentedFileConfig configData, final T value) {
configData.set(this.entry, value);
}
}
private static Object maxThreads(final Object value) {
int val = (Integer)value;
if (val <= 0) return Runtime.getRuntime().availableProcessors();
else return val;
}
private static final Logger LOGGER = LogUtils.getLogger();
private static FMLConfig INSTANCE = new FMLConfig();
private static ConfigSpec configSpec = new ConfigSpec();
private static final FMLConfig INSTANCE = new FMLConfig();
private static final ConfigSpec configSpec = new ConfigSpec();
private static final CommentedConfig configComments = CommentedConfig.inMemory();
static {
configSpec.define("splashscreen", Boolean.TRUE);
configSpec.define("maxThreads", -1);
configSpec.define("versionCheck", Boolean.TRUE);
configSpec.define("defaultConfigPath", "defaultconfigs");
configSpec.define("disableOptimizedDFU", Boolean.TRUE);
for (ConfigValue cv: ConfigValue.values()) {
cv.buildConfigEntry(configSpec, configComments);
}
}
private CommentedFileConfig configData;
@@ -38,7 +97,6 @@ public class FMLConfig
{
configData = CommentedFileConfig.builder(configFile).sync()
.onFileNotFound(FileNotFoundAction.copyData(Objects.requireNonNull(getClass().getResourceAsStream("/META-INF/defaultfmlconfig.toml"))))
.autosave().autoreload()
.writingMode(WritingMode.REPLACE)
.build();
try
@@ -47,13 +105,14 @@ public class FMLConfig
}
catch (ParsingException e)
{
throw new RuntimeException("Failed to load FML config from " + configFile.toString(), e);
throw new RuntimeException("Failed to load FML config from " + configFile, e);
}
if (!configSpec.isCorrect(configData)) {
LOGGER.warn(CORE, "Configuration file {} is not correct. Correcting", configFile);
configSpec.correct(configData, (action, path, incorrectValue, correctedValue) ->
LOGGER.warn(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue));
LOGGER.info(CORE, "Incorrect key {} was corrected from {} to {}", path, incorrectValue, correctedValue));
}
configData.putAllComments(configComments);
configData.save();
}
@@ -64,33 +123,34 @@ public class FMLConfig
if (LOGGER.isTraceEnabled(CORE))
{
LOGGER.trace(CORE, "Loaded FML config from {}", FMLPaths.FMLCONFIG.get());
LOGGER.trace(CORE, "Splash screen is {}", FMLConfig.splashScreenEnabled());
LOGGER.trace(CORE, "Max threads for mod loading computed at {}", FMLConfig.loadingThreadCount());
LOGGER.trace(CORE, "Version check is {}", FMLConfig.runVersionCheck());
LOGGER.trace(CORE, "Default config paths at {}", FMLConfig.defaultConfigPath());
for (ConfigValue cv: ConfigValue.values()) {
LOGGER.trace(CORE, "FMLConfig {} is {}", cv.entry, cv.getConfigValue(INSTANCE.configData));
}
}
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.defaultConfigPath()), "default config directory");
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH)));
}
public static boolean splashScreenEnabled() {
return INSTANCE.configData.<Boolean>getOptional("splashscreen").orElse(Boolean.FALSE);
public static String getConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static int loadingThreadCount() {
int val = INSTANCE.configData.<Integer>getOptional("maxThreads").orElse(-1);
if (val <= 0) return Runtime.getRuntime().availableProcessors();
return val;
public static boolean getBoolConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static boolean runVersionCheck() {
return INSTANCE.configData.<Boolean>getOptional("versionCheck").orElse(Boolean.TRUE);
public static int getIntConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static <A> List<A> getListConfigValue(ConfigValue v) {
return v.getConfigValue(INSTANCE.configData);
}
public static <T> void updateConfig(ConfigValue v, T value) {
v.updateValue(INSTANCE.configData, value);
INSTANCE.configData.save();
}
public static String defaultConfigPath() {
return INSTANCE.configData.<String>getOptional("defaultConfigPath").orElse("defaultconfigs");
}
public static boolean isOptimizedDFUDisabled() {
return INSTANCE.configData.<Boolean>getOptional("disableOptimizedDFU").orElse(Boolean.TRUE);
return getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH);
}
}
@@ -8,45 +8,40 @@ package net.minecraftforge.fml.loading;
import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.api.*;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
import net.minecraftforge.fml.loading.moddiscovery.BackgroundScanHandler;
import net.minecraftforge.fml.loading.moddiscovery.ModDiscoverer;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.fml.loading.moddiscovery.ModValidator;
import net.minecraftforge.accesstransformer.service.AccessTransformerService;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.loading.progress.EarlyProgressVisualization;
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
import net.minecraftforge.fml.loading.targets.CommonLaunchHandler;
import net.minecraftforge.forgespi.Environment;
import net.minecraftforge.forgespi.coremod.ICoreModProvider;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
import static net.minecraftforge.fml.loading.LogMarkers.SCAN;
public class FMLLoader
{
public class FMLLoader {
private static final Logger LOGGER = LogUtils.getLogger();
private static AccessTransformerService accessTransformer;
private static ModDiscoverer modDiscoverer;
private static ICoreModProvider coreModProvider;
private static ILaunchPluginService eventBus;
private static LanguageLoadingProvider languageLoadingProvider;
private static Dist dist;
private static String naming;
private static LoadingModList loadingModList;
private static RuntimeDistCleaner runtimeDistCleaner;
private static Path gamePath;
private static VersionInfo versionInfo;
private static final VersionInfo versionInfo = VersionInfo.detect();
private static String launchHandlerName;
private static CommonLaunchHandler commonLaunchHandler;
public static Runnable progressWindowTick;
@@ -55,80 +50,75 @@ public class FMLLoader
private static boolean production;
private static IModuleLayerManager moduleLayerManager;
static void onInitialLoad(IEnvironment environment, Set<String> otherServices) throws IncompatibleEnvironmentException
{
final String version = LauncherVersion.getVersion();
LOGGER.debug(CORE,"FML {} loading", version);
final Package modLauncherPackage = ITransformationService.class.getPackage();
LOGGER.debug(CORE,"FML found ModLauncher version : {}", modLauncherPackage.getImplementationVersion());
if (!modLauncherPackage.isCompatibleWith("4.0")) {
LOGGER.error(CORE, "Found incompatible ModLauncher specification : {}, version {} from {}", modLauncherPackage.getSpecificationVersion(), modLauncherPackage.getImplementationVersion(), modLauncherPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible modlauncher found "+modLauncherPackage.getSpecificationVersion());
}
static void onInitialLoad(IEnvironment env, Set<String> otherServices) throws IncompatibleEnvironmentException {
LOGGER.debug(CORE, "Detected version data : {}", versionInfo);
LOGGER.debug(CORE, "FML {} loading", LauncherVersion.getVersion());
accessTransformer = (AccessTransformerService) environment.findLaunchPlugin("accesstransformer").orElseThrow(()-> {
LOGGER.error(CORE, "Access Transformer library is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing AccessTransformer, cannot run");
});
final Package atPackage = accessTransformer.getClass().getPackage();
LOGGER.debug(CORE,"FML found AccessTransformer version : {}", atPackage.getImplementationVersion());
if (!atPackage.isCompatibleWith("1.0")) {
LOGGER.error(CORE, "Found incompatible AccessTransformer specification : {}, version {} from {}", atPackage.getSpecificationVersion(), atPackage.getImplementationVersion(), atPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible accesstransformer found "+atPackage.getSpecificationVersion());
}
eventBus = environment.findLaunchPlugin("eventbus").orElseThrow(()-> {
LOGGER.error(CORE, "Event Bus library is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing EventBus, cannot run");
});
final Package eventBusPackage = eventBus.getClass().getPackage();
LOGGER.debug(CORE,"FML found EventBus version : {}", eventBusPackage.getImplementationVersion());
if (!eventBusPackage.isCompatibleWith("1.0")) {
LOGGER.error(CORE, "Found incompatible EventBus specification : {}, version {} from {}", eventBusPackage.getSpecificationVersion(), eventBusPackage.getImplementationVersion(), eventBusPackage.getImplementationVendor());
throw new IncompatibleEnvironmentException("Incompatible eventbus found "+eventBusPackage.getSpecificationVersion());
}
runtimeDistCleaner = (RuntimeDistCleaner)environment.findLaunchPlugin("runtimedistcleaner").orElseThrow(()-> {
LOGGER.error(CORE, "Dist Cleaner is missing, we need this to run");
return new IncompatibleEnvironmentException("Missing DistCleaner, cannot run!");
});
LOGGER.debug(CORE, "Found Runtime Dist Cleaner");
var coreModProviders = ServiceLoaderUtils.streamWithErrorHandling(ServiceLoader.load(FMLLoader.class.getModule().getLayer(), ICoreModProvider.class), sce -> LOGGER.error(CORE, "Failed to load a coremod library, expect problems", sce)).toList();
if (coreModProviders.isEmpty()) {
LOGGER.error(CORE, "Found no coremod provider. Cannot run");
throw new IncompatibleEnvironmentException("No coremod library found");
} else if (coreModProviders.size() > 1) {
LOGGER.error(CORE, "Found multiple coremod providers : {}. Cannot run", coreModProviders.stream().map(p -> p.getClass().getName()).collect(Collectors.toList()));
throw new IncompatibleEnvironmentException("Multiple coremod libraries found");
}
coreModProvider = coreModProviders.get(0);
final Package coremodPackage = coreModProvider.getClass().getPackage();
LOGGER.debug(CORE,"FML found CoreMod version : {}", coremodPackage.getImplementationVersion());
LOGGER.debug(CORE, "Found ForgeSPI package implementation version {}", Environment.class.getPackage().getImplementationVersion());
LOGGER.debug(CORE, "Found ForgeSPI package specification {}", Environment.class.getPackage().getSpecificationVersion());
if (Integer.parseInt(Environment.class.getPackage().getSpecificationVersion()) < 2) {
LOGGER.error(CORE, "Found an out of date ForgeSPI implementation: {}, loading cannot continue", Environment.class.getPackage().getSpecificationVersion());
throw new IncompatibleEnvironmentException("ForgeSPI is out of date, we cannot continue");
}
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 {
Class.forName("com.electronwill.nightconfig.core.Config", false, environment.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, environment.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.core.Config", false, env.getClass().getClassLoader());
Class.forName("com.electronwill.nightconfig.toml.TomlFormat", false, env.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
LOGGER.error(CORE, "Failed to load NightConfig");
throw new IncompatibleEnvironmentException("Missing NightConfig");
}
}
static void setupLaunchHandler(final IEnvironment environment, final Map<String, Object> arguments)
{
@SuppressWarnings("unchecked")
private static <T> T getPlugin(IEnvironment env, String id, String version, String name) throws IncompatibleEnvironmentException {
var plugin = env.findLaunchPlugin(id).orElse(null);
if (plugin == null) {
LOGGER.error(CORE, "{} library is missing, we need this to run", name);
throw new IncompatibleEnvironmentException("Missing " + name + ", cannot run");
}
checkPackage(plugin.getClass(), version, name);
return (T) plugin;
}
private static void checkPackage(Class<?> cls, String version, String name) throws IncompatibleEnvironmentException {
var pkg = cls.getPackage();
var info = JarVersionLookupHandler.getInfo(pkg);
LOGGER.debug(CORE, "Found {} version: {}", name, info.impl().version().orElse("MISSING"));
if (!pkg.isCompatibleWith(version)) {
LOGGER.error(CORE, "Found incompatible {} specification: {}, version {} from {}", name,
info.spec().version().orElse("MISSING"),
info.impl().version().orElse("MISSING"),
info.impl().vendor().orElse("MISSING")
);
throw new IncompatibleEnvironmentException("Incompatible " + name + " found " + info.spec().version().orElse("MISSING"));
}
}
private static <T> T getSingleService(Class<T> clazz, String name) throws IncompatibleEnvironmentException {
var providers = new ArrayList<T>();
for (var itr = ServiceLoader.load(FMLLoader.class.getModule().getLayer(), clazz).iterator(); itr.hasNext(); ) {
try {
providers.add(itr.next());
} catch (ServiceConfigurationError e) {
LOGGER.error(CORE, "Failed to load a " + name + " library, expect problems", e);
}
}
if (providers.isEmpty()) {
LOGGER.error(CORE, "Found no {} provider. Cannot run", name);
throw new IncompatibleEnvironmentException("No " + name + " library found");
} else if (providers.size() > 1) {
LOGGER.error(CORE, "Found multiple {} providers: {}. Cannot run", name, providers.stream().map(p -> p.getClass().getName()).toList());
throw new IncompatibleEnvironmentException("Multiple " + name + " libraries found");
}
return providers.get(0);
}
static void setupLaunchHandler(final IEnvironment environment, final Map<String, Object> arguments) {
final String launchTarget = environment.getProperty(IEnvironment.Keys.LAUNCHTARGET.get()).orElse("MISSING");
arguments.put("launchTarget", launchTarget);
final Optional<ILaunchHandlerService> launchHandler = environment.findLaunchHandler(launchTarget);
@@ -138,38 +128,33 @@ public class FMLLoader
throw new RuntimeException("Missing launch handler: " + launchTarget);
}
// TODO: [FML][Loader] What the fuck is the point of using a service if you require a specific concrete class
if (!(launchHandler.get() instanceof CommonLaunchHandler)) {
LOGGER.error(CORE, "Incompatible Launch handler found - type {}, cannot continue", launchHandler.get().getClass().getName());
throw new RuntimeException("Incompatible launch handler found");
}
commonLaunchHandler = (CommonLaunchHandler)launchHandler.get();
launchHandlerName = launchHandler.get().name();
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Paths.get(".").toAbsolutePath());
gamePath = environment.getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Path.of(".").toAbsolutePath());
naming = commonLaunchHandler.getNaming();
dist = commonLaunchHandler.getDist();
production = commonLaunchHandler.isProduction();
versionInfo = new VersionInfo(arguments);
StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Early Loading!"));
accessTransformer.getExtension().accept(Pair.of(naming, "srg"));
LOGGER.debug(CORE,"Received command line version data : {}", versionInfo);
accessTransformer.getExtension().accept(Map.entry(naming, "srg"));
runtimeDistCleaner.getExtension().accept(dist);
}
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments)
{
public static List<ITransformationService.Resource> beginModScan(final Map<String,?> arguments) {
LOGGER.debug(SCAN,"Scanning for Mod Locators");
modDiscoverer = new ModDiscoverer(arguments);
var modDiscoverer = new ModDiscoverer(arguments);
modValidator = modDiscoverer.discoverMods();
var pluginResources = modValidator.getPluginResources();
return List.of(pluginResources);
}
public static List<ITransformationService.Resource> completeScan(IModuleLayerManager layerManager) {
progressWindowTick = EarlyProgressVisualization.INSTANCE.accept(dist, commonLaunchHandler.isData(), versionInfo.mcVersion());
moduleLayerManager = layerManager;
languageLoadingProvider = new LanguageLoadingProvider();
backgroundScanHandler = modValidator.stage2Validation();
@@ -181,43 +166,34 @@ public class FMLLoader
return coreModProvider;
}
public static LanguageLoadingProvider getLanguageLoadingProvider()
{
public static LanguageLoadingProvider getLanguageLoadingProvider() {
return languageLoadingProvider;
}
static ModDiscoverer getModDiscoverer() {
return modDiscoverer;
}
public static CommonLaunchHandler getLaunchHandler() {
return commonLaunchHandler;
}
public static void addAccessTransformer(Path atPath, ModFile modName)
{
public static void addAccessTransformer(Path atPath, ModFile modName) {
LOGGER.debug(SCAN, "Adding Access Transformer in {}", modName.getFilePath());
accessTransformer.offerResource(atPath, modName.getFileName());
}
public static Dist getDist()
{
public static Dist getDist() {
return dist;
}
public static void beforeStart(ClassLoader launchClassLoader)
{
StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Launching minecraft"));
public static void beforeStart(ModuleLayer gameLayer) {
ImmediateWindowHandler.acceptGameLayer(gameLayer);
ImmediateWindowHandler.updateProgress("Launching minecraft");
progressWindowTick.run();
}
public static LoadingModList getLoadingModList()
{
public static LoadingModList getLoadingModList() {
return loadingModList;
}
public static Path getGamePath()
{
public static Path getGamePath() {
return gamePath;
}
@@ -234,7 +210,7 @@ public class FMLLoader
}
public static List<Map<String, String>> modLauncherModList() {
return Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElseGet(Collections::emptyList);
return Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.MODLIST.get()).orElse(List.of());
}
public static String launcherHandlerName() {
@@ -9,6 +9,8 @@ import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.api.IEnvironment;
import org.slf4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
@@ -57,9 +59,13 @@ public enum FMLPaths
for (FMLPaths path : FMLPaths.values())
{
path.absolutePath = rootPath.resolve(path.relativePath).toAbsolutePath().normalize();
if (path.isDirectory)
if (path.isDirectory && !Files.isDirectory(path.absolutePath))
{
FileUtils.getOrCreateDirectory(path.absolutePath, path.name());
try {
Files.createDirectories(path.absolutePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (LOGGER.isDebugEnabled(CORE))
{
@@ -68,8 +74,18 @@ public enum FMLPaths
}
}
public static Path getOrCreateGameRelativePath(Path path, String name) {
return FileUtils.getOrCreateDirectory(FMLPaths.GAMEDIR.get().resolve(path), name);
public static Path getOrCreateGameRelativePath(Path path) {
Path gameFolderPath = FMLPaths.GAMEDIR.get().resolve(path);
if (!Files.isDirectory(gameFolderPath)) {
try {
Files.createDirectories(gameFolderPath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return gameFolderPath;
}
public Path relative() {
@@ -7,7 +7,6 @@ package net.minecraftforge.fml.loading;
import com.mojang.logging.LogUtils;
import cpw.mods.modlauncher.api.*;
import joptsimple.ArgumentAcceptingOptionSpec;
import joptsimple.OptionSpecBuilder;
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
import net.minecraftforge.forgespi.Environment;
@@ -15,7 +14,6 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -24,36 +22,18 @@ import java.util.Set;
import java.util.function.BiFunction;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
public class FMLServiceProvider implements ITransformationService
{
public class FMLServiceProvider implements ITransformationService {
private static final Logger LOGGER = LogUtils.getLogger();
private ArgumentAcceptingOptionSpec<String> modsOption;
private ArgumentAcceptingOptionSpec<String> modListsOption;
private ArgumentAcceptingOptionSpec<String> mavenRootsOption;
private ArgumentAcceptingOptionSpec<String> forgeOption;
private ArgumentAcceptingOptionSpec<String> mcOption;
private ArgumentAcceptingOptionSpec<String> forgeGroupOption;
private ArgumentAcceptingOptionSpec<String> mcpOption;
private ArgumentAcceptingOptionSpec<String> mappingsOption;
private List<String> modsArgumentList;
private List<String> modListsArgumentList;
private List<String> mavenRootsArgumentList;
private String targetForgeVersion;
private String targetMcVersion;
private String targetMcpVersion;
private String targetMcpMappings;
private String targetForgeGroup;
private Map<String, Object> arguments;
private final Map<String, Object> arguments = new HashMap<>();
public FMLServiceProvider()
{
final String markerselection = System.getProperty("forge.logging.markers", "");
Arrays.stream(markerselection.split(",")).forEach(marker -> System.setProperty("forge.logging.marker." + marker.toLowerCase(Locale.ROOT), "ACCEPT"));
public FMLServiceProvider() {
var markers = System.getProperty("forge.logging.markers", "").split(",");
for (var marker : markers)
System.setProperty("forge.logging.marker." + marker.toLowerCase(Locale.ROOT), "ACCEPT");
}
@Override
public String name()
{
public String name() {
return "fml";
}
@@ -65,15 +45,6 @@ public class FMLServiceProvider implements ITransformationService
FMLConfig.load();
LOGGER.debug(CORE, "Preparing ModFile");
environment.computePropertyIfAbsent(Environment.Keys.MODFILEFACTORY.get(), k->ModFile::new);
arguments = new HashMap<>();
arguments.put("modLists", modListsArgumentList);
arguments.put("mods", modsArgumentList);
arguments.put("mavenRoots", mavenRootsArgumentList);
arguments.put("forgeVersion", targetForgeVersion);
arguments.put("forgeGroup", targetForgeGroup);
arguments.put("mcVersion", targetMcVersion);
arguments.put("mcpVersion", targetMcpVersion);
arguments.put("mcpMappings", targetMcpMappings);
LOGGER.debug(CORE, "Preparing launch handler");
FMLLoader.setupLaunchHandler(environment, arguments);
FMLEnvironment.setupInteropEnvironment(environment);
@@ -92,45 +63,14 @@ public class FMLServiceProvider implements ITransformationService
}
@Override
public void onLoad(IEnvironment environment, Set<String> otherServices) throws IncompatibleEnvironmentException
{
// LOGGER.debug("Injecting tracing printstreams for STDOUT/STDERR.");
// System.setOut(new TracingPrintStream(LogManager.getLogger("STDOUT"), System.out));
// System.setErr(new TracingPrintStream(LogManager.getLogger("STDERR"), System.err));
public void onLoad(IEnvironment environment, Set<String> otherServices) throws IncompatibleEnvironmentException {
FMLLoader.onInitialLoad(environment, otherServices);
}
@SuppressWarnings("rawtypes")
@Override
public void arguments(BiFunction<String, String, OptionSpecBuilder> argumentBuilder)
{
forgeOption = argumentBuilder.apply("forgeVersion", "Forge Version number").withRequiredArg().ofType(String.class).required();
forgeGroupOption = argumentBuilder.apply("forgeGroup", "Forge Group (for testing)").withRequiredArg().ofType(String.class).defaultsTo("net.minecraftforge");
mcOption = argumentBuilder.apply("mcVersion", "Minecraft Version number").withRequiredArg().ofType(String.class).required();
mcpOption = argumentBuilder.apply("mcpVersion", "MCP Version number").withRequiredArg().ofType(String.class).required();
mappingsOption = argumentBuilder.apply("mcpMappings", "MCP Mappings Channel and Version").withRequiredArg().ofType(String.class);
modsOption = argumentBuilder.apply("mods", "List of mods to add").withRequiredArg().ofType(String.class).withValuesSeparatedBy(",");
modListsOption = argumentBuilder.apply("modLists", "JSON modlists").withRequiredArg().ofType(String.class).withValuesSeparatedBy(",");
mavenRootsOption = argumentBuilder.apply("mavenRoots", "Maven root directories").withRequiredArg().ofType(String.class).withValuesSeparatedBy(",");
}
@Override
public void argumentValues(OptionResult option)
{
modsArgumentList = option.values(modsOption);
modListsArgumentList = option.values(modListsOption);
mavenRootsArgumentList = option.values(mavenRootsOption);
targetForgeVersion = option.value(forgeOption);
targetForgeGroup = option.value(forgeGroupOption);
targetMcVersion = option.value(mcOption);
targetMcpVersion = option.value(mcpOption);
targetMcpMappings = option.value(mappingsOption);
}
@Override
public @NotNull List<ITransformer> transformers()
{
public @NotNull List<ITransformer> transformers() {
LOGGER.debug(CORE, "Loading coremod transformers");
return new ArrayList<>(FMLLoader.getCoreModProvider().getCoreModTransformers());
}
}
@@ -1,69 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import static net.minecraftforge.fml.loading.LogMarkers.CORE;
public class FileUtils
{
private static final Logger LOGGER = LogUtils.getLogger();
public static Path getOrCreateDirectory(Path dirPath, String dirLabel) {
if (!Files.isDirectory(dirPath.getParent())) {
getOrCreateDirectory(dirPath.getParent(), "parent of "+dirLabel);
}
if (!Files.isDirectory(dirPath))
{
LOGGER.debug(CORE, "Making {} directory : {}", dirLabel, dirPath);
try {
Files.createDirectory(dirPath);
} catch (IOException e) {
if (e instanceof FileAlreadyExistsException) {
LOGGER.error(CORE, "Failed to create {} directory - there is a file in the way", dirLabel);
} else {
LOGGER.error(CORE, "Problem with creating {} directory (Permissions?)", dirLabel, e);
}
throw new RuntimeException("Problem creating directory", e);
}
LOGGER.debug(CORE, "Created {} directory : {}", dirLabel, dirPath);
} else {
LOGGER.debug(CORE, "Found existing {} directory : {}", dirLabel, dirPath);
}
return dirPath;
}
public static String fileExtension(final Path path) {
String fileName = path.getFileName().toString();
int idx = fileName.lastIndexOf('.');
if (idx > -1) {
return fileName.substring(idx+1);
} else {
return "";
}
}
public static boolean matchFileName(String path, String... matches) {
// Extract file name from path
String name = path.substring(Math.min(path.lastIndexOf(File.separatorChar) + 1, path.length()));
// Check if it contains any of the desired keywords
for (String match : matches) {
if (name.contains(match)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,160 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading;
import net.minecraftforge.fml.loading.progress.ProgressMeter;
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public class ImmediateWindowHandler {
private static final Logger LOGGER = LogManager.getLogger();
private static ImmediateWindowProvider provider;
private static ProgressMeter earlyProgress;
public static void load(final String launchTarget, final String[] arguments) {
if (!launchTarget.contains("client")) {
provider = new DummyProvider();
LOGGER.info("ImmediateWindowProvider not loading because launch target is {}", launchTarget);
} else if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_CONTROL)) {
provider = new DummyProvider();
LOGGER.info("ImmediateWindowProvider not loading because splash screen is disabled");
} else {
final var providername = FMLConfig.getConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_PROVIDER);
LOGGER.info("Loading ImmediateWindowProvider {}", providername);
for (var service : ServiceLoader.load(ImmediateWindowProvider.class)) {
if (providername.equals(service.name())) {
provider = service;
break;
}
}
if (provider == null) {
LOGGER.info("Failed to find ImmediateWindowProvider {}, disabling", providername);
provider = new DummyProvider();
}
}
// Only update config if the provider isn't the dummy provider
if (!Objects.equals(provider.name(), "dummyprovider"))
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_PROVIDER, provider.name());
FMLLoader.progressWindowTick = provider.initialize(arguments);
earlyProgress = StartupNotificationManager.addProgressBar("EARLY", 0);
earlyProgress.label("Bootstrapping Minecraft");
}
public static long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor) {
return provider.setupMinecraftWindow(width, height, title, monitor);
}
public static boolean positionWindow(Optional<Object> monitor,IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) {
return provider.positionWindow(monitor, widthSetter, heightSetter, xSetter, ySetter);
}
public static void updateFBSize(IntConsumer width, IntConsumer height) {
provider.updateFramebufferSize(width, height);
}
public static <T> Supplier<T> loadingOverlay(Supplier<?> mc, Supplier<?> ri, Consumer<Optional<Throwable>> ex, boolean fade) {
earlyProgress.complete();
return provider.loadingOverlay(mc, ri, ex, fade);
}
public static void acceptGameLayer(final ModuleLayer layer) {
provider.updateModuleReads(layer);
}
public static void renderTick() {
provider.periodicTick();
}
public static String getGLVersion() {
return provider.getGLVersion();
}
public static void updateProgress(final String message) {
earlyProgress.label(message);
}
private record DummyProvider() implements ImmediateWindowProvider {
private static Method NV_HANDOFF;
private static Method NV_POSITION;
private static Method NV_OVERLAY;
private static Method NV_VERSION;
@Override
public String name() {
return "dummyprovider";
}
@Override
public Runnable initialize(String[] args) {
return () -> {};
}
@Override
public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) {
}
@Override
public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor) {
try {
var longsupplier = (LongSupplier)NV_HANDOFF.invoke(null, width, height, title, monitor);
return longsupplier.getAsLong();
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
}
}
public boolean positionWindow(Optional<Object> monitor, IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter) {
try {
return (boolean)NV_POSITION.invoke(null, monitor, widthSetter, heightSetter, xSetter, ySetter);
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
}
}
@SuppressWarnings("unchecked")
public <T> Supplier<T> loadingOverlay(Supplier<?> mc, Supplier<?> ri, Consumer<Optional<Throwable>> ex, boolean fade) {
try {
return (Supplier<T>) NV_OVERLAY.invoke(null, mc, ri, ex, fade);
} catch (Throwable e) {
throw new IllegalStateException("How did you get here?", e);
}
}
@Override
public String getGLVersion() {
try {
return (String) NV_VERSION.invoke(null);
} catch (Throwable e) {
return "3.2"; // Vanilla sets 3.2 in com.mojang.blaze3d.platform.Window
}
}
@Override
public void updateModuleReads(final ModuleLayer layer) {
var fm = layer.findModule("net.minecraftforge.forge");
if (fm.isPresent()) {
getClass().getModule().addReads(fm.get());
var clz = fm.map(l -> Class.forName(l, "net.minecraftforge.client.loading.NoVizFallback")).orElseThrow();
var methods = Arrays.stream(clz.getMethods()).filter(m -> Modifier.isStatic(m.getModifiers())).collect(Collectors.toMap(Method::getName, Function.identity()));
NV_HANDOFF = methods.get("windowHandoff");
NV_OVERLAY = methods.get("loadingOverlay");
NV_POSITION = methods.get("windowPositioning");
NV_VERSION = methods.get("glVersion");
}
}
@Override
public void periodicTick() {
// NOOP
}
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.fml.loading;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
/**
* This is for allowing the plugging in of alternative early display implementations.
*
* They can be selected through the config value "earlyWindowProvider" which defaults to "fmlearlywindow" implemented by {@link net.minecraftforge.fml.earlydisplay.DisplayWindow}
*
* There are a few key things to keep in mind if following through on implementation. You cannot access the game state as it
* literally DOES NOT EXIST at the time this object is constructed. You have to be very careful about managing the handoff
* to mojang, be sure that if you're trying to tick your window in a background thread (a nice idea!) that you properly
* transition to the main thread before handoff is complete. Do note that in general, you should construct your GL objects
* on the MAIN thread before starting your ticker, to ensure MacOS compatibility.
*
* No doubt many more things can be said here.
*/
public interface ImmediateWindowProvider {
/**
* @return The name of this window provider. Do NOT use fmlearlywindow.
*/
String name();
/**
* This is called very early on to initialize ourselves. Use this to initialize the window and other GL core resources.
*
* One thing we want to ensure is that we try and create the highest GL_PROFILE we can accomplish.
* GLFW_CONTEXT_VERSION_MAJOR,GLFW_CONTEXT_VERSION_MINOR should be as high as possible on the created window,
* and it should have all the typical profile settings.
*
* @param arguments The arguments provided to the Java process. This is the entire command line, so you can process
* stuff from it.
* @return A runnable that will be periodically ticked by FML during startup ON THE MAIN THREAD. This is usually
* a good place to put glfwPollEvents() tests.
*/
Runnable initialize(String[] arguments);
/**
* This will be called during the handoff to minecraft to update minecraft with the size of the framebuffer we have.
* Generally won't be called because Minecraft figures it out for itself.
* @param width Consumer of the framebuffer width
* @param height Consumer of the framebuffer height
*/
void updateFramebufferSize(IntConsumer width, IntConsumer height);
/**
* This is called to setup the minecraft window, as if Mojang had done it themselves in their Window class. This
* handoff is difficult to get right - you have to make sure that any activities you're doing to the window are finished
* prior to returning. You should try and setup the width and height as Mojang expects - the suppliers give you all that
* information. Alternatively, you can force Mojang to update from the current position of the window in {@link #positionWindow(Optional, IntConsumer, IntConsumer, IntConsumer, IntConsumer)}
* instead. This might give a more seamless experience.
*
* @param width This is the width of the window Mojang expects
* @param height This is the height of the Window Mojang expects.
* @param title This is the title for the window.
* @param monitor This is the monitor it should appear on.
* @return The window id
*/
long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitor);
/**
* This is called after window handoff to allow us to tell Mojang about our window's position. This might give a
* preferrable user experience to users, because we just tell Mojang our truth, rather than accept theirs.
* @param monitor This is the monitor we're rendering on. Note that this is the Mojang monitor object. You might have trouble unwrapping it.
* @param widthSetter This sets the width on the Mojang side
* @param heightSetter This sets the height on the Mojang side
* @param xSetter This sets the x coordinate on the Mojang side
* @param ySetter This sets the y coordinate on the Mojang side
* @return true if you've handled the window positioning - this skips the "forced fullscreen" code until a later stage
*/
boolean positionWindow(Optional<Object> monitor, IntConsumer widthSetter, IntConsumer heightSetter, IntConsumer xSetter, IntConsumer ySetter);
/**
* Return a Supplier of an object extending the LoadingOverlay class from Mojang. This is what will be used once
* the Mojang window code has taken over rendering of the window, to render the later stages of the loading process.
*
* @param mc This supplies the Minecraft object
* @param ri This supplies the ReloadInstance object that tells us when the loading is finished
* @param ex This Consumes the final state of the loading - if it's an error you pass it the Throwable, otherwise you
* pass Optional.empty()
* @param fade This is the fade flag passed to LoadingOverlay. You probably want to ignore it.
* @param <T> This is the type LoadingOverlay to allow type binding on the Mojang side
* @return A supplier of your later LoadingOverlay screen.
*/
<T> Supplier<T> loadingOverlay(Supplier<?> mc, Supplier<?> ri, Consumer<Optional<Throwable>> ex, boolean fade);
/**
* This is called during the module loading process to allow us to find objects inside the GAME layer, such as a
* later loading screen.
* @param layer This is the GAME layer from ModLauncher
*/
void updateModuleReads(ModuleLayer layer);
/**
* This is called periodically during the loading process to "tick" the window. It is typically the same as the Runnable
* from {@link #initialize(String[])}
*/
void periodicTick();
/**
* This is called to construct a {@link net.minecraftforge.forgespi.locating.ForgeFeature} for the GL_VERSION we
* managed to create for the window. Should be a string of the format {MAJOR}.{MINOR}, such as 4.6, 4.5 or such.
*
* @return the GL profile we created
*/
String getGLVersion();
}
Loaded 100 of 3277 files, more files were not shown because too many files have changed in this diff. Show more