1+ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
12import org.jetbrains.gradle.ext.Gradle
23
34plugins {
@@ -95,6 +96,10 @@ java {
9596configurations {
9697 contain
9798 implementation. extendsFrom(contain)
99+ // External classes bundled into the mod jar body under the shared shadow namespace, mirroring
100+ // the upstream Celeritas distribution layout. Kept separate from implementation so the plain
101+ // namespace jars stay on the compile/dev classpath for third-party mods that need them.
102+ shadowBundle
98103 modCompileOnly
99104 compileOnly. extendsFrom(modCompileOnly)
100105 modRuntimeOnly
@@ -281,6 +286,35 @@ def actiniumProjectAccessTransformer = propertyBool('use_access_transformer')
281286 ? rootProject. projectDir. toPath(). resolve(" src/main/resources/${ propertyString('access_transformer_locations')} " ). toFile()
282287 : null
283288
289+ /**
290+ * Dev-runtime image of the mod in the same shadow layout as the production jar: merged module
291+ * classes plus the shadow bundle classes relocated under
292+ * org.embeddedt.embeddium.impl.shadow.joml. The client and server runs swap the raw source-set
293+ * outputs and the plain joml jar for this artifact so mixin field descriptors seen at dev runtime
294+ * match the production distribution. The plain joml jar itself stays on the run classpath for
295+ * third-party mods that still speak org.joml.
296+ */
297+ def devShadowJar = tasks. register(' devShadowJar' , ShadowJar ) {
298+ group = ' build'
299+ description = ' Builds the shadow-layout dev runtime jar consumed by the client and server runs.'
300+ archiveBaseName. set(' Actinium-dev' )
301+ archiveClassifier. set(' ' )
302+ archiveVersion. set(' ' )
303+ destinationDirectory. set(layout. buildDirectory. dir(' dev-libs' ))
304+ configurations. set([project. configurations. shadowBundle])
305+ from(sourceSets. main. output)
306+ relocate(' org.joml' , ' org.embeddedt.embeddium.impl.shadow.joml' )
307+ mergeServiceFiles()
308+ // Deliberately no manifest inheritance from the jar task: that manifest carries the
309+ // FMLCorePlugin / FMLCorePluginContainsFMLMod entries the production jar needs, but at dev time
310+ // the coremod is already provided through unimined's fml.coreMods.load system property. Letting
311+ // them surface on a plain classpath jar makes FML's classpath scan treat this jar as a coremod
312+ // container, which parent-delegates its package (class loader exclusion) and splits classes
313+ // like MixinLate across the app class loader and the LaunchClassLoader (ClassCastException on
314+ // the mixinbooter ILateMixinLoader cast).
315+ manifest. attributes(' Implementation-Version' : finalVersion)
316+ }
317+
284318def remapCompatBridgeJar
285319
286320unimined. minecraft {
@@ -304,12 +338,32 @@ unimined.minecraft {
304338 jvmArgs + = extraArgs. split(' \\ s+' ). toList()
305339 }
306340
341+ // Swap the raw module outputs for the shadow-layout dev jar so the dev runtime carries
342+ // the same relocated joml descriptors as the production distribution. The swap happens
343+ // on the -Dcrl.dev.extrapath channel below, NOT on the JavaExec classpath: mod classes
344+ // must stay invisible to the app class loader (exactly like production, where the mod
345+ // jar is not on java.class.path). Keeping the jar on the JavaExec classpath lets the
346+ // app loader serve Actinium classes as a parent-delegation fallback and splits classes
347+ // like MixinLate across the app loader and the LaunchClassLoader (ClassCastException on
348+ // the mixinbooter ILateMixinLoader cast).
349+ def originalRunClasspath = classpath
350+ classpath = files(provider {
351+ def rawModuleOutputs = sourceSets. main. output. files
352+ originalRunClasspath. files. findAll { file ->
353+ ! rawModuleOutputs. contains(file)
354+ }
355+ })
356+
307357 // Unimined omits source output paths here when a clean build has not created their directories yet.
358+ // The raw module outputs must not reach Cleanroom either: they would expose unrelocated
359+ // joml descriptors next to the shadowed ones, so the dev shadow jar replaces them here.
308360 def extraPathArgument = ' -Dcrl.dev.extrapath='
361+ def rawModuleOutputPaths = project. sourceSets. main. output. files. collect { it. absolutePath } as Set
309362 def extraPaths = new LinkedHashSet<String > ()
310- extraPaths. addAll(project . sourceSets . main . output . files . collect { it . absolutePath } )
363+ extraPaths. add(devShadowJar . get() . archiveFile . get() . asFile . absolutePath)
311364 jvmArgs. findAll { it. toString(). startsWith(extraPathArgument) }. each { argument ->
312- extraPaths. addAll(argument. toString(). substring(extraPathArgument. length()). tokenize(File . pathSeparator))
365+ extraPaths. addAll(argument. toString(). substring(extraPathArgument. length()). tokenize(File . pathSeparator)
366+ .findAll { path -> ! rawModuleOutputPaths. contains(path) })
313367 }
314368 jvmArgs = jvmArgs. findAll { ! it. toString(). startsWith(extraPathArgument) }
315369 jvmArgs + = " ${ extraPathArgument}${ extraPaths.join(File.pathSeparator)} "
@@ -327,6 +381,10 @@ unimined.minecraft {
327381 enableBaseMixin()
328382 enableMixinExtra()
329383 }
384+ // Thin intermediate: the distributed jar is the shadowRemapJar output.
385+ asJar {
386+ archiveClassifier. set(' thin' )
387+ }
330388 }
331389
332390 remapCompatBridgeJar = remap(compatBridgeJar. get(), ' remapCompatBridgeJar' ) {
@@ -351,6 +409,25 @@ unimined.minecraft {
351409
352410def compatBridgeRemapOutput = remapCompatBridgeJar. flatMap { it. archiveFile }
353411
412+ /**
413+ * Production mod jar in the upstream Celeritas distribution layout: the remapped thin jar merged
414+ * with the shadow bundle classes, with joml relocated under
415+ * org.embeddedt.embeddium.impl.shadow.joml so mixin field descriptors match the layout upstream
416+ * Celeritas addons bind against. The ContainedDeps joml jar inherited from the thin jar stays
417+ * nested for third-party mods that still consume plain org.joml.
418+ */
419+ def shadowRemapJar = tasks. register(' shadowRemapJar' , ShadowJar ) {
420+ group = ' build'
421+ description = ' Builds the production mod jar with the upstream shadow layout (relocated joml).'
422+ dependsOn tasks. named(' remapJar' )
423+ archiveClassifier. set(' ' )
424+ configurations. set([project. configurations. shadowBundle])
425+ from(zipTree(tasks. named(' remapJar' ). get(). archiveFile))
426+ relocate(' org.joml' , ' org.embeddedt.embeddium.impl.shadow.joml' )
427+ mergeServiceFiles()
428+ manifest. inheritFrom(tasks. named(' jar' ). get(). manifest)
429+ }
430+
354431/**
355432 * Chunk Animator's dev-environment coremod (MCPNames) reads ./../mcp/methods.csv and
356433 * ./../mcp/fields.csv relative to the client run directory and crashes at class init when they
@@ -414,15 +491,18 @@ def prepareCompatBridgeRun = tasks.register('prepareCompatBridgeRun') {
414491
415492tasks. named(' preRunClient' ). configure {
416493 dependsOn tasks. named(' classes' )
494+ dependsOn devShadowJar
417495 dependsOn prepareCompatBridgeRun
418496 dependsOn prepareChunkAnimatorMcpMappings
419497}
420498
421499tasks. named(' preRunServer' ). configure {
422500 dependsOn tasks. named(' classes' )
501+ dependsOn devShadowJar
423502}
424503
425504tasks. named(' assemble' ). configure {
505+ dependsOn shadowRemapJar
426506 dependsOn remapCompatBridgeJar
427507 dependsOn compatBridgeSourcesJar
428508}
@@ -619,13 +699,13 @@ tasks.named('processResources').configure {
619699 from(' THIRD_PARTY_NOTICES.md' , ' LICENSE-REESES-SODIUM-OPTIONS.md' )
620700}
621701
622- def verifyRemapJar = tasks. register(' verifyRemapJar ' ) {
623- dependsOn tasks. named(' remapJar ' )
624- def remapOutput = tasks. named(' remapJar ' ). flatMap { it. archiveFile }
625- inputs. file(remapOutput )
702+ def verifyDistributedJar = tasks. register(' verifyDistributedJar ' ) {
703+ dependsOn tasks. named(' shadowRemapJar ' )
704+ def shadowOutput = tasks. named(' shadowRemapJar ' ). flatMap { it. archiveFile }
705+ inputs. file(shadowOutput )
626706
627707 doLast {
628- def archive = zipTree(remapOutput . get(). asFile)
708+ def archive = zipTree(shadowOutput . get(). asFile)
629709 def requiredEntries = [
630710 ' META-INF/MANIFEST.MF' ,
631711 ' THIRD_PARTY_NOTICES.md' ,
@@ -634,31 +714,39 @@ def verifyRemapJar = tasks.register('verifyRemapJar') {
634714 ' com/gtnewhorizons/angelica/glsm/GLStateManager.class' ,
635715 ' net/coderbot/iris/Iris.class' ,
636716 ' org/embeddedt/embeddium/impl/render/chunk/RenderSectionManager.class' ,
717+ ' org/embeddedt/embeddium/impl/shadow/joml/Vector3f.class' ,
718+ ' joml-1.10.5.jar' ,
637719 ' mixins.actinium.iris.json' ,
638720 ' mixins.actinium.vintage.json' ,
639721 ' META-INF/services/com.gtnewhorizons.angelica.glsm.backend.RenderBackend'
640722 ]
641723
642724 requiredEntries. each { entry ->
643725 if (archive. matching { include entry }. isEmpty()) {
644- throw new GradleException (" Remapped mod jar is missing required entry: ${ entry} " )
726+ throw new GradleException (" Distributed mod jar is missing required entry: ${ entry} " )
645727 }
646728 }
647729
648- def manifest = new java.util.jar.JarFile (remapOutput. get(). asFile). withCloseable {
730+ // The shadow layout only holds relocated joml classes in the jar body; plain org.joml is
731+ // provided to third-party mods by the nested ContainedDeps jar instead.
732+ if (! archive. matching { include ' org/joml/**/*.class' }. isEmpty()) {
733+ throw new GradleException (' Distributed mod jar leaks plain org.joml classes into the jar body' )
734+ }
735+
736+ def manifest = new java.util.jar.JarFile (shadowOutput. get(). asFile). withCloseable {
649737 it. manifest. mainAttributes
650738 }
651739 if (manifest. getValue(' FMLCorePlugin' ) != propertyString(' coremod_plugin_class_name' )) {
652- throw new GradleException (' Remapped mod jar has an invalid FMLCorePlugin manifest entry' )
740+ throw new GradleException (' Distributed mod jar has an invalid FMLCorePlugin manifest entry' )
653741 }
654742 if (manifest. getValue(' FMLAT' ) != propertyString(' access_transformer_locations' ). tokenize(' /\\ ' ). last()) {
655- throw new GradleException (' Remapped mod jar has an invalid FMLAT manifest entry' )
743+ throw new GradleException (' Distributed mod jar has an invalid FMLAT manifest entry' )
656744 }
657745
658- new java.util.jar.JarFile (remapOutput . get(). asFile). withCloseable { jar ->
746+ new java.util.jar.JarFile (shadowOutput . get(). asFile). withCloseable { jar ->
659747 def metadataEntry = jar. getEntry(' mcmod.info' )
660748 if (metadataEntry == null ) {
661- throw new GradleException (' Remapped mod jar is missing mcmod.info' )
749+ throw new GradleException (' Distributed mod jar is missing mcmod.info' )
662750 }
663751 def metadata = new groovy.json.JsonSlurper (). parse(jar. getInputStream(metadataEntry))
664752 def mod = metadata[0 ]
@@ -669,10 +757,10 @@ def verifyRemapJar = tasks.register('verifyRemapJar') {
669757 || mod. url != propertyString(' mod_url' )
670758 || mod. updateJSON != propertyString(' mod_update_json' )
671759 || mod. logoFile != propertyString(' mod_logo_path' )) {
672- throw new GradleException (' Remapped mod jar metadata does not match Gradle mod metadata properties' )
760+ throw new GradleException (' Distributed mod jar metadata does not match Gradle mod metadata properties' )
673761 }
674762 if (! mod. logoFile. isEmpty() && jar. getEntry(mod. logoFile) == null ) {
675- throw new GradleException (" Remapped mod jar is missing configured logo: ${ mod.logoFile} " )
763+ throw new GradleException (" Distributed mod jar is missing configured logo: ${ mod.logoFile} " )
676764 }
677765 }
678766 }
@@ -740,7 +828,7 @@ def verifyModuleBoundaries = tasks.register('verifyModuleBoundaries') {
740828}
741829
742830tasks. named(' check' ). configure {
743- dependsOn verifyRemapJar
831+ dependsOn verifyDistributedJar
744832 dependsOn verifyCompatBridgeJar
745833 dependsOn verifyModuleBoundaries
746834}
0 commit comments