diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5d81c7ee..6012765c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,15 +1,22 @@ name: Bug Report description: Report an issue you've encountered -labels: [bug] +labels: [ bug ] body: - type: markdown attributes: - value: "## Welcome!" + value: "## 👋 Welcome!" - type: markdown attributes: value: | - Thank you for taking your time to report this bug! - Make sure you are running the latest version before reporting. + ### 📋 Checklist + Thank you for taking your time to report this bug! + Before reporting, please make ensure that: + - You are using the **latest** available version of Connector and its dependencies + - You've installed a **minimal set of mods** required to reproduce the issue. + Issues with modpacks and excessive amount of mods will *not* be accepted, as they take a long time to diagnose. + Knowing which mods are causing problems allows us to focus on fixing the issue as soon as possible. + If you're unsure which mods might be at fault, try using [binary search](https://www.reddit.com/r/feedthebeast/comments/evpy6r/tips_for_modpack_authors_how_to_find_misbehaving/) - removing half of installed mods + repeatedly until the faulty mod is found. - type: input id: description attributes: @@ -36,10 +43,10 @@ body: attributes: label: Logs description: | - If applicable (crash, error output in console), please provide your crash report or latest.log - To upload logs, use an external site to post the crash report, such as [Ubuntu Pastebin](https://paste.ubuntu.com/), [Pastebin](http://pastebin.com/), or [Github Gist](https://gist.github.com/) + If applicable (crash, error output in console), please provide your debug.log **and** crash report + To upload logs, use an external paste site, such as [Github Gist](https://gist.github.com/) (recommended), [Ubuntu Pastebin](https://paste.ubuntu.com/) or [Pastebin](http://pastebin.com) - type: textarea id: context attributes: label: Additional context - description: "Add any other context about the problem here, such as the modpack you're playing or other mods that might be causing this bug." \ No newline at end of file + description: "Add any other context about the problem here, such as your current environment or other mods that might be causing this bug." \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/mod_incompatibility.yml b/.github/ISSUE_TEMPLATE/mod_incompatibility.yml index fa1cf049..aeff5949 100644 --- a/.github/ISSUE_TEMPLATE/mod_incompatibility.yml +++ b/.github/ISSUE_TEMPLATE/mod_incompatibility.yml @@ -4,12 +4,19 @@ labels: [compat] body: - type: markdown attributes: - value: "## Welcome!" + value: "## 👋 Welcome!" - type: markdown attributes: value: | - Thank you for taking your time to report this incompatibility! - Make sure you are running the latest version of Connector and its dependencies before reporting. + ### 📋 Checklist + Thank you for taking your time to report this bug! + Before reporting, please make ensure that: + - You are using the **latest** available version of Connector and its dependencies + - You've installed a **minimal set of mods** required to reproduce the issue. + Issues with modpacks and excessive amount of mods will *not* be accepted, as they take a long time to diagnose. + Knowing which mods are causing problems allows us to focus on fixing the issue as soon as possible. + If you're unsure which mods might be at fault, try using [binary search](https://www.reddit.com/r/feedthebeast/comments/evpy6r/tips_for_modpack_authors_how_to_find_misbehaving/) - removing half of installed mods + repeatedly until the faulty mod is found. - type: input id: mod attributes: @@ -51,13 +58,13 @@ body: attributes: label: Logs description: | - If applicable (crash, error output in console), please provide your crash report. + If applicable (crash, error output in console), please provide your debug.log **and** crash report. Otherwise, we recommend uploading the debug.log found in your `.minecraft/logs` directory. - To upload logs, use an external site to post the crash report, such as [Ubuntu Pastebin](https://paste.ubuntu.com/), [Pastebin](http://pastebin.com/), or [Github Gist](https://gist.github.com/) + To upload logs, use an external paste site, such as [Github Gist](https://gist.github.com/) (recommended), [Ubuntu Pastebin](https://paste.ubuntu.com/) or [Pastebin](http://pastebin.com/) validations: required: true - type: textarea id: context attributes: label: Additional context - description: "Add any other context about the problem here, such as the modpack you're playing or other mods that might be causing this bug." \ No newline at end of file + description: "Add any other context about the problem here, such as your current environment or other mods that might be causing this bug." \ No newline at end of file diff --git a/README.md b/README.md index 28b987e8..4689966e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ on [MinecraftForge](https://minecraftforge.net). Its goal is to bring the two pl developers time and effort maintaining their mods for multiple platforms at once, as well as allowing players to play all their favourite mods in one modpack. -### Recommendations +### 🔗 Related Projects - Visit the [Mod Compatibility Thread](https://github.com/Sinytra/Connector/discussions/12) to can find information about known working / incompatible mods - To learn more about how Connector works, read our [Introductory blog post](https://github.com/Sinytra/Connector/discussions/11) diff --git a/build.gradle.kts b/build.gradle.kts index 55830e22..5f4d8ec1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,13 +2,16 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import me.modmuss50.mpp.ReleaseType import net.minecraftforge.gradle.common.util.MavenArtifactDownloader import net.minecraftforge.gradle.common.util.RunConfig +import net.minecraftforge.gradle.userdev.tasks.JarJar import net.minecraftforge.gradle.userdev.util.MavenPomUtils import net.minecraftforge.jarjar.metadata.* import org.apache.maven.artifact.versioning.DefaultArtifactVersion import org.apache.maven.artifact.versioning.VersionRange +import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.StandardOpenOption import java.time.LocalDateTime +import kotlin.io.path.inputStream plugins { java @@ -28,6 +31,7 @@ val versionMc: String by project val versionForge: String by project val versionForgeAutoRenamingTool: String by project val versionFabricLoader: String by project +val versionFabricLoaderUpstream: String by project val versionAccessWidener: String by project val versionFabricApi: String by project val versionMixin: String by project @@ -91,33 +95,26 @@ val dummyFabricLoaderLangJar: Jar by tasks.creating(Jar::class) { ) archiveClassifier.set("fabricloader") } -// Generate JarJar metadata manually so that we control both the version and the file path -val createJarJarMetadata: Task by tasks.creating { - val jarPath = "META-INF/jarjar/" + dummyFabricLoaderLangJar.archiveFile.get().asFile.name - val output = project.layout.buildDirectory.dir("createJarJarMetadata").get().file("metadata.json") - inputs.property("jarPath", jarPath) - outputs.file(output) - extra["output"] = output - doFirst { - val metadata = Metadata( - listOf( - ContainedJarMetadata( - ContainedJarIdentifier("dev.su5ed.sinytra", "fabric-loader"), - ContainedVersion(VersionRange.createFromVersion("[$dummyFabricLoaderVersion,)"), DefaultArtifactVersion(dummyFabricLoaderVersion)), - jarPath, - false - ) - ) - ) - Files.deleteIfExists(output.asFile.toPath()) - Files.write(output.asFile.toPath(), MetadataIOHandler.toLines(metadata), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) - } -} -val modJar: Jar by tasks.creating(Jar::class) { +val modJar: JarJar by tasks.creating(JarJar::class) { + dependsOn(dummyFabricLoaderLangJar) from(mod.output) - into("META-INF/jarjar/") { - from(createJarJarMetadata) - from(dummyFabricLoaderLangJar) + inputs.file(dummyFabricLoaderLangJar.archiveFile) + configurations = listOf(project.configurations.jarJar.get()) + (includedDependencies as ConfigurableFileCollection).from(dummyFabricLoaderLangJar) + doLast { + FileSystems.newFileSystem(archiveFile.get().asFile.toPath()).use { fs -> + val jarPath = "META-INF/jarjar/" + dummyFabricLoaderLangJar.archiveFile.get().asFile.name + val metadataPath = fs.getPath("META-INF/jarjar/metadata.json") + val metadata = metadataPath.inputStream().use { ins -> MetadataIOHandler.fromStream(ins).orElseThrow() } + metadata.jars() += ContainedJarMetadata( + ContainedJarIdentifier("dev.su5ed.sinytra", "fabric-loader"), + ContainedVersion(VersionRange.createFromVersion("[$dummyFabricLoaderVersion,)"), DefaultArtifactVersion(dummyFabricLoaderVersion)), + jarPath, + false + ) + Files.deleteIfExists(metadataPath) + Files.write(metadataPath, MetadataIOHandler.toLines(metadata), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + } } manifest.attributes( "Implementation-Version" to project.version, @@ -153,6 +150,7 @@ val fullJar: Jar by tasks.creating(Jar::class) { manifest { from(tasks.jar.get().manifest) attributes("Embedded-Dependencies-Mod" to "META-INF/jarjar/" + modJar.archiveFile.get().asFile.name) + attributes("Fabric-Loader-Version" to versionFabricLoaderUpstream) } } @@ -274,8 +272,11 @@ dependencies { shade(group = "io.github.steelwoolmc", name = "mixin-transmogrifier", version = versionMixinTransmog) adapterData(group = "dev.su5ed.sinytra.adapter", name = "adapter", version = versionAdapter) - compileOnly(group = "net.fabricmc", name = "sponge-mixin", version = versionMixin) annotationProcessor(group = "net.fabricmc", name = "sponge-mixin", version = versionMixin) + compileOnly(group = "net.fabricmc", name = "sponge-mixin", version = versionMixin) + implementation(jarJar("io.github.llamalad7:mixinextras-forge:0.3.1")!!) { + jarJar.ranged(this, "[0.3.1,)") + } compileOnly(group = "dev.su5ed.sinytra.fabric-api", name = "fabric-api", version = versionFabricApi) runtimeOnly(fg.deobf("dev.su5ed.sinytra.fabric-api:fabric-api:$versionFabricApi")) diff --git a/gradle.properties b/gradle.properties index 5d74320d..acaf24cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,18 +4,19 @@ org.gradle.jvmargs=-Xmx3G org.gradle.daemon=true # Versions -versionConnector=1.0.0-beta.27 +versionConnector=1.0.0-beta.28 versionAdapter=1.8.16-1.20.1-20231120.221506 -versionAdapterDefinition=1.8.20 +versionAdapterDefinition=1.8.21 versionMc=1.20.1 versionForge=47.1.3 versionForgeAutoRenamingTool=1.0.9 -versionFabricLoader=2.5.2+0.14.21+1.20.1 +versionFabricLoader=2.6.0+0.15.0+1.20.1 +versionFabricLoaderUpstream=0.15.0 versionAccessWidener=2.1.0 versionFabricApi=0.90.7+1.9.33+1.20.1 versionMixin=0.12.5+mixin.0.8.5 -versionMixinTransmog=0.4.1+1.20.1 +versionMixinTransmog=0.4.2+1.20.1 # Publishing curseForgeId=890127 diff --git a/src/main/java/dev/su5ed/sinytra/connector/loader/ConnectorEarlyLoader.java b/src/main/java/dev/su5ed/sinytra/connector/loader/ConnectorEarlyLoader.java index f0a50b2c..f50fda2a 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/loader/ConnectorEarlyLoader.java +++ b/src/main/java/dev/su5ed/sinytra/connector/loader/ConnectorEarlyLoader.java @@ -3,9 +3,9 @@ import com.mojang.logging.LogUtils; import dev.su5ed.sinytra.connector.ConnectorUtil; import dev.su5ed.sinytra.connector.locator.ConnectorConfig; +import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; import net.fabricmc.loader.impl.FabricLoaderImpl; -import net.fabricmc.loader.impl.entrypoint.EntrypointUtils; import net.minecraftforge.fml.loading.EarlyLoadingException; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; @@ -128,7 +128,7 @@ public static void preLaunch() { ProgressMeter progress = StartupNotificationManager.addProgressBar("[Connector] PreLaunch", 0); try { // Invoke prelaunch entrypoint - EntrypointUtils.invoke("preLaunch", PreLaunchEntrypoint.class, PreLaunchEntrypoint::onPreLaunch); + FabricLoader.getInstance().invokeEntrypoints("preLaunch", PreLaunchEntrypoint.class, PreLaunchEntrypoint::onPreLaunch); } catch (Throwable t) { LOGGER.error("Encountered an error in prelaunch entrypoint", t); addGenericLoadingException(t, "Encountered an error in prelaunch entrypoint"); diff --git a/src/main/java/dev/su5ed/sinytra/connector/locator/DependencyResolver.java b/src/main/java/dev/su5ed/sinytra/connector/locator/DependencyResolver.java index 1dd6e67e..79262805 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/locator/DependencyResolver.java +++ b/src/main/java/dev/su5ed/sinytra/connector/locator/DependencyResolver.java @@ -102,7 +102,8 @@ private static ModCandidate createJavaMod() { } private static ModCandidate createFabricLoaderMod() { - ModMetadata metadata = new BuiltinModMetadata.Builder("fabricloader", Objects.requireNonNullElse(FabricLoader.class.getPackage().getImplementationVersion(), "0.0NONE")) + String version = EmbeddedDependencies.getFabricLoaderVersion(); + ModMetadata metadata = new BuiltinModMetadata.Builder("fabricloader", Objects.requireNonNullElse(version, "0.0NONE")) .setName("Fabric Loader") .build(); GameProvider.BuiltinMod builtinMod = new GameProvider.BuiltinMod(Collections.singletonList(Path.of(uncheck(() -> FabricLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI()))), metadata); diff --git a/src/main/java/dev/su5ed/sinytra/connector/locator/EmbeddedDependencies.java b/src/main/java/dev/su5ed/sinytra/connector/locator/EmbeddedDependencies.java index 1645c4fc..9f3d663a 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/locator/EmbeddedDependencies.java +++ b/src/main/java/dev/su5ed/sinytra/connector/locator/EmbeddedDependencies.java @@ -34,6 +34,8 @@ public final class EmbeddedDependencies { private static final Logger LOGGER = LogUtils.getLogger(); // Manifest attribute name prefix for embedded dependencies private static final String JIJ_ATTRIBUTE_PREFIX = "Embedded-Dependencies-"; + // Fabric Loader upstream version included by Connector + private static final String FABRIC_LOADER_VERSION = "Fabric-Loader-Version"; // Embedded mod jar name private static final String MOD_JIJ_DEP = "Mod"; @@ -72,6 +74,10 @@ public static String getJarCacheVersion() { return JAR_CACHE_VERSION.get(); } + public static String getFabricLoaderVersion() { + return ATTRIBUTES.getValue(FABRIC_LOADER_VERSION); + } + /** * Get the root path inside an embedded jar. * diff --git a/src/main/java/dev/su5ed/sinytra/connector/service/ConnectorLoaderService.java b/src/main/java/dev/su5ed/sinytra/connector/service/ConnectorLoaderService.java index f3fad136..5ff9639a 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/service/ConnectorLoaderService.java +++ b/src/main/java/dev/su5ed/sinytra/connector/service/ConnectorLoaderService.java @@ -20,6 +20,7 @@ public class ConnectorLoaderService implements ITransformationService { private static final String NAME = "connector_loader"; + private static final String AUTHLIB_MODULE = "authlib"; @Override public String name() { @@ -55,10 +56,11 @@ public void onLoad(IEnvironment env, Set otherServices) { Map plugins = (Map) pluginsField.get(launchPluginHandler); // Sort launch plugins LinkedHashMap sortedPlugins = new LinkedHashMap<>(); - // Runtime Enum extender will fail if a mixin makes $VALUES mutable first, so it must come before us as well - sortedPlugins.put("runtime_enum_extender", plugins.remove("runtime_enum_extender")); - // Mixin must come after + // Mixin must come first sortedPlugins.put("mixin", plugins.remove("mixin")); + // Handle cases where a mixin has already made the enum mutable + plugins.remove("runtime_enum_extender"); + sortedPlugins.put("runtime_enum_extender", new LenientRuntimeEnumExtender()); // Our plugins come after mixin injectPlugins.forEach(plugin -> sortedPlugins.put(plugin.name(), plugin)); // The rest goes to the end @@ -72,7 +74,10 @@ public void onLoad(IEnvironment env, Set otherServices) { @Override public List completeScan(IModuleLayerManager layerManager) { LoadingModList.get().getErrors().addAll(ConnectorEarlyLoader.getLoadingExceptions()); - return List.of(new Resource(IModuleLayerManager.Layer.GAME, List.of(new FabricASMFixer.FabricASMGeneratedClassesSecureJar()))); + return List.of(new Resource(IModuleLayerManager.Layer.GAME, List.of( + new FabricASMFixer.FabricASMGeneratedClassesSecureJar(), + ModuleLayerMigrator.moveModule(AUTHLIB_MODULE) + ))); } @Override diff --git a/src/main/java/dev/su5ed/sinytra/connector/service/LenientRuntimeEnumExtender.java b/src/main/java/dev/su5ed/sinytra/connector/service/LenientRuntimeEnumExtender.java new file mode 100644 index 00000000..3cac25ce --- /dev/null +++ b/src/main/java/dev/su5ed/sinytra/connector/service/LenientRuntimeEnumExtender.java @@ -0,0 +1,33 @@ +package dev.su5ed.sinytra.connector.service; + +import net.minecraftforge.fml.common.asm.RuntimeEnumExtender; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; + +import java.util.List; + +public class LenientRuntimeEnumExtender extends RuntimeEnumExtender { + @Override + public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) { + if ((classNode.access & Opcodes.ACC_ENUM) == 0) + return ComputeFlags.NO_REWRITE; + // Modified query flags that do not include ACC_FINAL + int flags = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC; + Type array = Type.getType("[" + classType.getDescriptor()); + List values = classNode.fields.stream().filter(f -> f.desc.contentEquals(array.getDescriptor()) && (f.access & flags) == flags) + .toList(); + if (values.size() == 1) { + FieldNode node = values.get(0); + if ((node.access & Opcodes.ACC_FINAL) == 0) { + // It is likely a mixin already made the field mutable + // Make it final before it is processed and de-finalized again by super + node.access |= Opcodes.ACC_FINAL; + return super.processClassWithFlags(phase, classNode, classType, reason); + } + } + // Let super deal with this + return super.processClassWithFlags(phase, classNode, classType, reason); + } +} diff --git a/src/main/java/dev/su5ed/sinytra/connector/service/ModuleLayerMigrator.java b/src/main/java/dev/su5ed/sinytra/connector/service/ModuleLayerMigrator.java new file mode 100644 index 00000000..235c57c3 --- /dev/null +++ b/src/main/java/dev/su5ed/sinytra/connector/service/ModuleLayerMigrator.java @@ -0,0 +1,161 @@ +package dev.su5ed.sinytra.connector.service; + +import com.mojang.logging.LogUtils; +import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import org.slf4j.Logger; +import sun.misc.Unsafe; + +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleReference; +import java.lang.module.ResolvedModule; +import java.lang.reflect.Field; +import java.net.URI; +import java.nio.file.Path; +import java.security.CodeSigner; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import static cpw.mods.modlauncher.api.LamdbaExceptionUtils.uncheck; + +public class ModuleLayerMigrator { + private static final MethodHandles.Lookup TRUSTED_LOOKUP = uncheck(() -> { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + Unsafe unsafe = (Unsafe) theUnsafe.get(null); + Field hackfield = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + return (MethodHandles.Lookup) unsafe.getObject(unsafe.staticFieldBase(hackfield), unsafe.staticFieldOffset(hackfield)); + }); + private static final Class JAR_MODULE_REF_CLASS = uncheck(() -> Class.forName("cpw.mods.cl.JarModuleFinder$JarModuleReference")); + private static final VarHandle REF_MODULE_PROVIDER_FIELD = uncheck(() -> TRUSTED_LOOKUP.findVarHandle(JAR_MODULE_REF_CLASS, "jar", SecureJar.ModuleDataProvider.class)); + private static final VarHandle DESCRIPTOR_PACKAGES_FIELD = uncheck(() -> TRUSTED_LOOKUP.findVarHandle(ModuleDescriptor.class, "packages", Set.class)); + private static final Logger LOGGER = LogUtils.getLogger(); + + /** + * "Moves" a module from the {@link cpw.mods.modlauncher.api.IModuleLayerManager.Layer#BOOT BOOT} layer to {@link cpw.mods.modlauncher.api.IModuleLayerManager.Layer#GAME GAME} + * in order to make it transformable. This is achieved by disabling the old module and preventing it from loading its classes, then adding a new one to the upper layer + * to load classes from instead. + *

+ * Libraries transferred by this method are only used by minecraft after the {@link cpw.mods.modlauncher.api.IModuleLayerManager.Layer#GAME GAME} layer becomes available. + * In theory, all of this should work just fine without breaking other mods. + * + * @param moduleName name of the module to transfer + * @return the new jar to be placed on the {@link cpw.mods.modlauncher.api.IModuleLayerManager.Layer#GAME GAME} layer + */ + public static SecureJar moveModule(String moduleName) { + try { + LOGGER.debug("Attempting to make module {} transformable", moduleName); + ModuleLayer layer = Launcher.INSTANCE.findLayerManager().orElseThrow().getLayer(IModuleLayerManager.Layer.BOOT).orElseThrow(); + ResolvedModule module = layer.configuration().findModule(moduleName).orElseThrow(() -> new RuntimeException("Module %s not found".formatted(moduleName))); + Module actualModule = layer.findModule(moduleName).orElseThrow(() -> new RuntimeException("Module %s not found".formatted(moduleName))); + + ModuleReference reference = module.reference(); + if (!JAR_MODULE_REF_CLASS.isInstance(reference)) { + throw new RuntimeException("Module %s does not contain a jar module reference".formatted(moduleName)); + } + + // Replace module provider with an empty one to avoid loading classes twice + SecureJar.ModuleDataProvider originalProvider = (SecureJar.ModuleDataProvider) REF_MODULE_PROVIDER_FIELD.get(reference); + SecureJar.ModuleDataProvider wrappedProvider = new EmptyModuleDataProvider(originalProvider.name()); + REF_MODULE_PROVIDER_FIELD.set(reference, wrappedProvider); + + // Remove all packages from the original module to avoid split package conflicts + ModuleDescriptor desc = actualModule.getDescriptor(); + DESCRIPTOR_PACKAGES_FIELD.set(desc, Set.of()); + + LOGGER.info("Successfully made module {} transformable", moduleName); + SecureJar.ModuleDataProvider provider = new ModuleDataProviderWrapper(originalProvider, "connector$" + moduleName); + return new SimpleSecureJar(provider); + } catch (Throwable t) { + throw new RuntimeException("Error making module %s transformable".formatted(moduleName), t); + } + } + + private static class EmptyModuleDataProvider implements SecureJar.ModuleDataProvider { + private final String name; + private ModuleDescriptor descriptor; + + public EmptyModuleDataProvider(String name) { + this.name = name; + } + + @Override + public ModuleDescriptor descriptor() { + if (descriptor == null) { + descriptor = ModuleDescriptor.newAutomaticModule(name()).build(); + } + return descriptor; + } + + //@formatter:off + @Override public String name() {return this.name;} + @Override public URI uri() {return uncheck(() -> new URI("file:///~nonexistent"));} + @Override public Optional findFile(String name) {return Optional.empty();} + @Override public Optional open(String name) {return Optional.empty();} + @Override public Manifest getManifest() {return new Manifest();} + @Override public CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes) {return new CodeSigner[0];} + //@formatter:on + } + + private record SimpleSecureJar(ModuleDataProvider moduleDataProvider) implements SecureJar { + //@formatter:off + @Override public Path getPrimaryPath() {return Path.of(moduleDataProvider().uri());} + @Override public CodeSigner[] getManifestSigners() {return new CodeSigner[0];} + @Override public Status verifyPath(Path path) {return Status.NONE;} + @Override public Status getFileStatus(String name) {return Status.NONE;} + @Override public Attributes getTrustedManifestEntries(String name) {return new Attributes();} + @Override public boolean hasSecurityData() {return false;} + @Override public Set getPackages() {return moduleDataProvider().descriptor().packages();} + @Override public List getProviders() {return List.of();} + @Override public String name() {return moduleDataProvider().name();} + @Override public Path getPath(String first, String... rest) {return getPrimaryPath();} + @Override public Path getRootPath() {return getPrimaryPath();} + //@formatter:on + } + + private static class ModuleDataProviderWrapper implements SecureJar.ModuleDataProvider { + private final SecureJar.ModuleDataProvider provider; + private final String name; + private ModuleDescriptor descriptor; + + public ModuleDataProviderWrapper(SecureJar.ModuleDataProvider provider, String name) { + this.provider = provider; + this.name = name; + } + + @Override + public ModuleDescriptor descriptor() { + if (descriptor == null) { + ModuleDescriptor desc = this.provider.descriptor(); + var builder = ModuleDescriptor.newModule(this.name, desc.modifiers()); + builder.packages(desc.packages()); + if (!desc.isAutomatic()) { + desc.version().ifPresent(builder::version); + desc.requires().forEach(builder::requires); + desc.exports().forEach(builder::exports); + desc.opens().forEach(builder::opens); + desc.uses().forEach(builder::uses); + desc.provides().forEach(builder::provides); + } + descriptor = builder.build(); + } + return descriptor; + } + + //@formatter:off + @Override public String name(){return name;} + @Override public URI uri() {return provider.uri();} + @Override public Optional findFile(String name) {return provider.findFile(name);} + @Override public Optional open(String name) {return provider.open(name);} + @Override public Manifest getManifest() {return provider.getManifest();} + @Override public CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes) {return provider.verifyAndGetSigners(cname, bytes);} + //@formatter:on + } +} diff --git a/src/main/java/dev/su5ed/sinytra/connector/transformer/AccessWidenerTransformer.java b/src/main/java/dev/su5ed/sinytra/connector/transformer/AccessWidenerTransformer.java index 6905bcf2..41587496 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/transformer/AccessWidenerTransformer.java +++ b/src/main/java/dev/su5ed/sinytra/connector/transformer/AccessWidenerTransformer.java @@ -1,5 +1,6 @@ package dev.su5ed.sinytra.connector.transformer; +import dev.su5ed.sinytra.connector.transformer.jar.IntermediateMapping; import net.fabricmc.accesswidener.AccessWidenerReader; import net.fabricmc.accesswidener.AccessWidenerVisitor; import net.fabricmc.loader.impl.MappingResolverImpl; @@ -14,12 +15,12 @@ public class AccessWidenerTransformer implements Transformer { private final String resource; private final MappingResolverImpl resolver; - private final Map flatMapping; + private final IntermediateMapping fastMapping; - public AccessWidenerTransformer(String resource, MappingResolverImpl namedMappingFile, Map flatMapping) { + public AccessWidenerTransformer(String resource, MappingResolverImpl namedMappingFile, IntermediateMapping fastMapping) { this.resource = resource; this.resolver = namedMappingFile; - this.flatMapping = flatMapping; + this.fastMapping = fastMapping; } @Override @@ -102,7 +103,7 @@ public void visitMethod(String owner, String name, String descriptor, AccessWide String mappedName = AccessWidenerTransformer.this.resolver.mapMethodName(this.sourceNamespace, owner, name, descriptor); // Mods might target inherited methods that are not part of the mapping file, we'll try to remap them using the flat mapping instead if (name.equals(mappedName)) { - mappedName = AccessWidenerTransformer.this.flatMapping.getOrDefault(name, name); + mappedName = AccessWidenerTransformer.this.fastMapping.mapMethodOrDefault(name, descriptor); } String mappedDescriptor = AccessWidenerTransformer.this.resolver.mapDescriptor(this.sourceNamespace, descriptor); this.builder.append(modifier).append(" ") diff --git a/src/main/java/dev/su5ed/sinytra/connector/transformer/MixinPatchTransformer.java b/src/main/java/dev/su5ed/sinytra/connector/transformer/MixinPatchTransformer.java index c367d6e1..48cfe102 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/transformer/MixinPatchTransformer.java +++ b/src/main/java/dev/su5ed/sinytra/connector/transformer/MixinPatchTransformer.java @@ -76,6 +76,12 @@ public class MixinPatchTransformer implements Transformer { .targetInjectionPoint("Lnet/fabricmc/loader/impl/game/minecraft/Hooks;startServer(Ljava/io/File;Ljava/lang/Object;)V") .modifyInjectionPoint("Lnet/minecraftforge/server/loading/ServerModLoader;load()V") .build(), + Patch.builder() + .targetClass("net/minecraft/world/item/ShovelItem") + .targetMethod("m_6225_") + .targetInjectionPoint("Lnet/minecraft/world/level/block/state/BlockState;m_60795_()Z") + .modifyInjectionPoint("Lnet/minecraft/world/level/Level;m_46859_(Lnet/minecraft/core/BlockPos;)Z") + .build(), Patch.builder() .targetClass("net/minecraft/world/level/NaturalSpawner") .targetMethod("m_220443_") @@ -161,6 +167,14 @@ public class MixinPatchTransformer implements Transformer { .targetInjectionPoint("Lnet/minecraft/client/renderer/LightTexture;m_252983_(Lorg/joml/Vector3f;)V") .modifyParams(builder -> builder.substitute(17, 16)) .build(), + // This code is being removed by forge, mixins to it can be safely disabled + Patch.builder() + .targetClass("net/minecraft/server/players/PlayerList") + .targetMethod("m_11239_") + .targetInjectionPoint("Lnet/minecraft/world/entity/player/Player;m_7755_()Lnet/minecraft/network/chat/Component;") + .targetMixinType(Patch.REDIRECT) + .disable() + .build(), // TODO Automate Patch.builder() .targetClass("net/minecraft/client/Minecraft") diff --git a/src/main/java/dev/su5ed/sinytra/connector/transformer/OptimizedRenamingTransformer.java b/src/main/java/dev/su5ed/sinytra/connector/transformer/OptimizedRenamingTransformer.java index 71c12032..713472a5 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/transformer/OptimizedRenamingTransformer.java +++ b/src/main/java/dev/su5ed/sinytra/connector/transformer/OptimizedRenamingTransformer.java @@ -2,6 +2,7 @@ import dev.su5ed.sinytra.adapter.patch.selector.AnnotationHandle; import dev.su5ed.sinytra.adapter.patch.util.MethodQualifier; +import dev.su5ed.sinytra.connector.transformer.jar.IntermediateMapping; import net.minecraftforge.fart.api.ClassProvider; import net.minecraftforge.fart.api.Transformer; import net.minecraftforge.fart.internal.ClassProviderImpl; @@ -36,14 +37,13 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.function.UnaryOperator; import java.util.stream.Stream; public final class OptimizedRenamingTransformer extends RenamingTransformer { private static final String CLASS_DESC_PATTERN = "^L[a-zA-Z0-9/$_]+;$"; private static final String FQN_CLASS_NAME_PATTERN = "^([a-zA-Z0-9$_]+\\.)*[a-zA-Z0-9$_]+$"; - public static Transformer create(ClassProvider classProvider, Consumer log, IMappingFile mappingFile, Map flatMappings) { + public static Transformer create(ClassProvider classProvider, Consumer log, IMappingFile mappingFile, IntermediateMapping flatMappings) { RenamingClassProvider reverseProvider = new RenamingClassProvider(classProvider, mappingFile, mappingFile.reverse(), log); EnhancedRemapper enhancedRemapper = new RelocatingEnhancedRemapper(reverseProvider, mappingFile, flatMappings, log); return new OptimizedRenamingTransformer(enhancedRemapper, false); @@ -88,17 +88,19 @@ protected void postProcess(ClassNode node) { } private static class PostProcessRemapper { - private final Map flatMappings; + private final IntermediateMapping flatMappings; private final Remapper remapper; - public PostProcessRemapper(Map flatMappings, Remapper remapper) { + public PostProcessRemapper(IntermediateMapping flatMappings, Remapper remapper) { this.flatMappings = flatMappings; this.remapper = remapper; } public void mapAnnotationValues(List values) { - for (int i = 1; i < values.size(); i += 2) { - values.set(i, mapAnnotationValue(values.get(i))); + if (values != null) { + for (int i = 1; i < values.size(); i += 2) { + values.set(i, mapAnnotationValue(values.get(i))); + } } } @@ -118,13 +120,13 @@ else if (obj instanceof List list) { public Object mapValue(Object value) { if (value instanceof String str) { if (str.matches(CLASS_DESC_PATTERN)) { - String mapped = flatMappings.get(str.substring(1, str.length() - 1)); + String mapped = flatMappings.map(str.substring(1, str.length() - 1)); if (mapped != null) { return 'L' + mapped + ';'; } } else if (str.matches(FQN_CLASS_NAME_PATTERN)) { - String mapped = flatMappings.get(str.replace('.', '/')); + String mapped = flatMappings.map(str.replace('.', '/')); if (mapped != null) { return mapped.replace('/', '.'); } @@ -133,12 +135,12 @@ else if (str.matches(FQN_CLASS_NAME_PATTERN)) { MethodQualifier qualifier = MethodQualifier.create(str).orElse(null); if (qualifier != null && qualifier.desc() != null) { String owner = qualifier.owner() != null ? this.remapper.mapDesc(qualifier.owner()) : ""; - String name = qualifier.name() != null ? this.flatMappings.getOrDefault(qualifier.name(), qualifier.name()) : ""; - String desc = qualifier.desc() != null ? this.remapper.mapMethodDesc(qualifier.desc()) : ""; + String name = qualifier.name() != null ? this.flatMappings.mapMethodOrDefault(qualifier.name(), qualifier.desc()) : ""; + String desc = this.remapper.mapMethodDesc(qualifier.desc()); return owner + name + desc; } - String mapped = this.flatMappings.get(str); + String mapped = this.flatMappings.map(str); if (mapped != null) { return mapped; } @@ -193,16 +195,16 @@ public void close() throws IOException { } private static class RelocatingEnhancedRemapper extends EnhancedRemapper { - private final Map flatMappings; + private final IntermediateMapping flatMappings; - public RelocatingEnhancedRemapper(ClassProvider classProvider, IMappingFile map, Map flatMappings, Consumer log) { + public RelocatingEnhancedRemapper(ClassProvider classProvider, IMappingFile map, IntermediateMapping flatMappings, Consumer log) { super(classProvider, map, log); this.flatMappings = flatMappings; } @Override public String map(final String key) { - String fastMapped = this.flatMappings.get(key); + String fastMapped = this.flatMappings.map(key); if (fastMapped != null) { return fastMapped; } @@ -211,7 +213,7 @@ public String map(final String key) { @Override public String mapFieldName(String owner, String name, String descriptor) { - String fastMapped = this.flatMappings.get(name); + String fastMapped = this.flatMappings.mapField(name, descriptor); if (fastMapped != null) { return fastMapped; } @@ -232,6 +234,10 @@ public String mapFieldName(String owner, String name, String descriptor) { @Override public String mapMethodName(String owner, String name, String descriptor) { + String fastMapped = this.flatMappings.mapMethod(name, descriptor); + if (fastMapped != null) { + return fastMapped; + } return this.classProvider.getClass(owner) .map(cls -> { // Handle methods belonging to interfaces added through @Implements @@ -239,12 +245,12 @@ public String mapMethodName(String owner, String name, String descriptor) { int interfacePrefix = name.indexOf("$"); if (interfacePrefix > -1 && name.lastIndexOf("$") == interfacePrefix) { String actualName = name.substring(interfacePrefix + 1); - String fastMapped = this.flatMappings.get(actualName); - String mapped = fastMapped != null ? fastMapped : mapMethodName(owner, actualName, descriptor); + String fastMappedLambda = this.flatMappings.mapMethod(actualName, descriptor); + String mapped = fastMappedLambda != null ? fastMappedLambda : mapMethodName(owner, actualName, descriptor); return name.substring(0, interfacePrefix + 1) + mapped; } } - return this.flatMappings.get(name); + return null; }) .orElseGet(() -> super.mapMethodName(owner, name, descriptor)); } diff --git a/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/IntermediateMapping.java b/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/IntermediateMapping.java index 4b7ff8ab..0442a9f4 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/IntermediateMapping.java +++ b/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/IntermediateMapping.java @@ -1,22 +1,23 @@ package dev.su5ed.sinytra.connector.transformer.jar; -import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import net.fabricmc.loader.impl.FabricLoaderImpl; import net.fabricmc.loader.impl.MappingResolverImpl; +import net.minecraftforge.srgutils.IMappingFile; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Stream; import static dev.su5ed.sinytra.connector.transformer.jar.JarTransformer.TRANSFORM_MARKER; public class IntermediateMapping { - private static final Map> INTERMEDIATE_MAPPINGS_CACHE = new HashMap<>(); + private static final Map INTERMEDIATE_MAPPINGS_CACHE = new HashMap<>(); // Filter out non-obfuscated method names used in mapping namespaces as those don't need // to be remapped and will only cause issues with our barebones find/replace remapper private static final Map> MAPPING_PREFIXES = Map.of( @@ -24,39 +25,95 @@ public class IntermediateMapping { ); private static final Logger LOGGER = LogUtils.getLogger(); - public static Map get(String sourceNamespace) { - Map map = INTERMEDIATE_MAPPINGS_CACHE.get(sourceNamespace); - if (map == null) { + // Original -> Mapped + private final Map mappings; + // Original + Descriptor -> Mapped + private final Map extendedMappings; + + public static IntermediateMapping get(String sourceNamespace) { + IntermediateMapping existing = INTERMEDIATE_MAPPINGS_CACHE.get(sourceNamespace); + if (existing == null) { synchronized (JarTransformer.class) { - Map existing = INTERMEDIATE_MAPPINGS_CACHE.get(sourceNamespace); + existing = INTERMEDIATE_MAPPINGS_CACHE.get(sourceNamespace); if (existing != null) { return existing; } LOGGER.debug(TRANSFORM_MARKER, "Creating flat intermediate mapping for namespace {}", sourceNamespace); // Intermediary sometimes contains duplicate names for different methods (why?). We exclude those. - Set excludedNames = new HashSet<>(); Collection prefixes = MAPPING_PREFIXES.get(sourceNamespace); MappingResolverImpl resolver = FabricLoaderImpl.INSTANCE.getMappingResolver(); Map resolved = new HashMap<>(); + Map buffer = new HashMap<>(); + Map extendedMappings = new HashMap<>(); resolver.getCurrentMap(sourceNamespace).getClasses().stream() .flatMap(cls -> Stream.concat(Stream.of(cls), Stream.concat(cls.getFields().stream(), cls.getMethods().stream())) - .filter(node -> prefixes.stream().anyMatch(node.getOriginal()::startsWith)) - .map(node -> Pair.of(node.getOriginal(), node.getMapped()))) - .forEach(pair -> { - String original = pair.getFirst(); - if (resolved.containsKey(original)) { - excludedNames.add(original); + .filter(node -> prefixes.stream().anyMatch(node.getOriginal()::startsWith))) + .forEach(node -> { + String original = node.getOriginal(); + String mapped = node.getMapped(); + String mapping = resolved.get(original); + if (mapping != null && !mapping.equals(mapped)) { resolved.remove(original); + extendedMappings.put(getMappingKey(buffer.remove(original)), mapping); + extendedMappings.put(getMappingKey(node), mapped); } - if (!excludedNames.contains(original)) { - resolved.put(original, pair.getSecond()); + else if (!extendedMappings.containsKey(getMappingKey(node))) { + resolved.put(original, mapped); + buffer.put(original, node); } }); - INTERMEDIATE_MAPPINGS_CACHE.put(sourceNamespace, resolved); - return resolved; + IntermediateMapping mapping = new IntermediateMapping(resolved, extendedMappings); + INTERMEDIATE_MAPPINGS_CACHE.put(sourceNamespace, mapping); + return mapping; } } - return map; + return existing; + } + + private static String getMappingKey(IMappingFile.INode node) { + if (node instanceof IMappingFile.IField field) { + String desc = field.getDescriptor(); + return field.getOriginal() + (desc != null ? ":" + desc : ""); + } + else if (node instanceof IMappingFile.IMethod method) { + return method.getOriginal() + Objects.requireNonNullElse(method.getDescriptor(), ""); + } + return node.getOriginal(); + } + + public IntermediateMapping(Map mappings, Map extendedMappings) { + this.mappings = mappings; + this.extendedMappings = extendedMappings; + } + + @Nullable + public String map(String name) { + return this.mappings.get(name); + } + + @Nullable + public String mapField(String name, @Nullable String desc) { + String mapped = this.mappings.get(name); + if (mapped == null) { + String qualifier = name + ":" + desc; + return this.extendedMappings.get(qualifier); + } + return mapped; + } + + public String mapMethodOrDefault(String name, String desc) { + String mapped = mapMethod(name, desc); + return mapped != null ? mapped : name; + } + + @Nullable + public String mapMethod(String name, String desc) { + String mapped = this.mappings.get(name); + if (mapped == null) { + String qualifier = name + desc; + return this.extendedMappings.get(qualifier); + } + return mapped; } } diff --git a/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/JarTransformer.java b/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/JarTransformer.java index a24fd736..27bd75f0 100644 --- a/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/JarTransformer.java +++ b/src/main/java/dev/su5ed/sinytra/connector/transformer/jar/JarTransformer.java @@ -66,6 +66,7 @@ public final class JarTransformer { // Keep this outside of BytecodeFixerUpperFrontend to prevent unnecessary static init of patches when we only need the jar path private static final Path GENERATED_JAR_PATH = ConnectorUtil.CONNECTOR_FOLDER.resolve("adapter/adapter_generated_mixins.jar"); private static final String LOOM_GENERATED_PROPERTY = "fabric-loom:generated"; + private static final String LOOM_REMAP_ATTRIBUTE = "Fabric-Loom-Remap"; private static final Logger LOGGER = LogUtils.getLogger(); private static final VarHandle TRANSFORMER_LOADER_FIELD = uncheck(() -> MethodHandles.privateLookupIn(MixinLaunchPluginLegacy.class, MethodHandles.lookup()).findVarHandle(MixinLaunchPluginLegacy.class, "transformerLoader", ILaunchPluginService.ITransformerLoader.class)); @@ -203,12 +204,20 @@ private static FabricModFileMetadata readModMetadata(File input) throws IOExcept } }); Attributes manifestAttributes = Optional.ofNullable(jarFile.getManifest()).map(Manifest::getMainAttributes).orElseGet(Attributes::new); - CustomValue generatedValue = metadata.getCustomValue(LOOM_GENERATED_PROPERTY); - boolean generated = generatedValue != null && generatedValue.getType() == CustomValue.CvType.BOOLEAN && generatedValue.getAsBoolean(); + boolean generated = isGeneratedLibraryJarMetadata(manifestAttributes, metadata); return new FabricModFileMetadata(metadata, visibleConfigs, configs, refmaps, mixinPackages, manifestAttributes, containsAT, generated); } } + private static boolean isGeneratedLibraryJarMetadata(Attributes manifestAttributes, LoaderModMetadata metadata) { + CustomValue generatedValue = metadata.getCustomValue(LOOM_GENERATED_PROPERTY); + if (generatedValue != null && generatedValue.getType() == CustomValue.CvType.BOOLEAN && generatedValue.getAsBoolean()) { + String loomRemapAttribute = manifestAttributes.getValue(LOOM_REMAP_ATTRIBUTE); + return loomRemapAttribute == null || !loomRemapAttribute.equals("true"); + } + return false; + } + private static void readMixinConfigPackages(File input, JarFile jarFile, ZipEntry entry, Set refmaps, Set packages) { try (Reader reader = new InputStreamReader(jarFile.getInputStream(entry))) { JsonObject json = JsonParser.parseReader(reader).getAsJsonObject(); diff --git a/src/mod/java/dev/su5ed/sinytra/connector/mod/ConnectorLoader.java b/src/mod/java/dev/su5ed/sinytra/connector/mod/ConnectorLoader.java index d4d884bc..020c72b0 100644 --- a/src/mod/java/dev/su5ed/sinytra/connector/mod/ConnectorLoader.java +++ b/src/mod/java/dev/su5ed/sinytra/connector/mod/ConnectorLoader.java @@ -6,7 +6,7 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.api.ModInitializer; -import net.fabricmc.loader.impl.entrypoint.EntrypointUtils; +import net.fabricmc.loader.api.FabricLoader; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.progress.ProgressMeter; @@ -49,12 +49,13 @@ public static void load() { LazyEntityAttributes.inject(); // Invoke entry points - EntrypointUtils.invoke("main", ModInitializer.class, ModInitializer::onInitialize); + FabricLoader loader = FabricLoader.getInstance(); + loader.invokeEntrypoints("main", ModInitializer.class, ModInitializer::onInitialize); if (FMLEnvironment.dist == Dist.CLIENT) { - EntrypointUtils.invoke("client", ClientModInitializer.class, ClientModInitializer::onInitializeClient); + loader.invokeEntrypoints("client", ClientModInitializer.class, ClientModInitializer::onInitializeClient); } else { - EntrypointUtils.invoke("server", DedicatedServerModInitializer.class, DedicatedServerModInitializer::onInitializeServer); + loader.invokeEntrypoints("server", DedicatedServerModInitializer.class, DedicatedServerModInitializer::onInitializeServer); } LazyEntityAttributes.release();