From 4fe9d8318a1625040f6909550adacb85226f5992 Mon Sep 17 00:00:00 2001 From: IcyNova4 <261982305+IcyNova4@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:39:52 -0500 Subject: [PATCH] Fix GameTest startup stability across classpath edge cases - parse built-in mod URLs using URI paths on Windows - guard shared scan data writes in concurrent class scanning - skip environment-mismatched client/server class loading during auto registration - harden remapper classpath library resolution for jar/file providers - add defensive Fabric API redirects for missing jar filesystem providers - ensure Loom runs include jdk.zipfs module --- build.gradle.kts | 8 ++- .../fabric_api/ModNioResourcePackMixin.java | 24 +++++++ .../fabric_api/ServerLanguageUtilMixin.java | 24 +++++++ .../xyz/bluspring/kilt/loader/KiltLoader.kt | 42 +++++++++++- .../kilt/loader/remap/KiltRemapper.kt | 64 +++++++++++++++++-- src/main/resources/Kilt.mixins.json | 2 + 6 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ModNioResourcePackMixin.java create mode 100644 src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ServerLanguageUtilMixin.java diff --git a/build.gradle.kts b/build.gradle.kts index d825693ad..20f3144d7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,6 +78,12 @@ loom { "MISSING_INJECTOR_DESC_SINGLETARGET" to "error" )) } + + runs { + configureEach { + vmArg("--add-modules=jdk.zipfs") + } + } } allprojects { @@ -690,4 +696,4 @@ fun getVersionMetadata(): String { System.getenv("GITHUB_SHA") ?: grgit.head().abbreviatedId return "+build.${commitHash.subSequence(0, 6)}${if (System.getenv("GITHUB_RUN_NUMBER") == null) "-local" else if (!isRelease()) "-nightly" else ""}" -} \ No newline at end of file +} diff --git a/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ModNioResourcePackMixin.java b/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ModNioResourcePackMixin.java new file mode 100644 index 000000000..33e0d3268 --- /dev/null +++ b/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ModNioResourcePackMixin.java @@ -0,0 +1,24 @@ +package xyz.bluspring.kilt.mixin.compat.fabric_api; + +import net.fabricmc.fabric.impl.resource.loader.ModNioResourcePack; +import net.fabricmc.loader.api.ModContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.util.Collections; +import java.util.List; + +@Mixin(value = ModNioResourcePack.class, remap = false) +public class ModNioResourcePackMixin { + @Redirect(method = "create", at = @At(value = "INVOKE", target = "Lnet/fabricmc/loader/api/ModContainer;getRootPaths()Ljava/util/List;")) + private static List kilt$skipMissingJarFileSystems(ModContainer instance) { + try { + return instance.getRootPaths(); + } catch (ProviderNotFoundException exception) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ServerLanguageUtilMixin.java b/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ServerLanguageUtilMixin.java new file mode 100644 index 000000000..93ec0a19e --- /dev/null +++ b/src/main/java/xyz/bluspring/kilt/mixin/compat/fabric_api/ServerLanguageUtilMixin.java @@ -0,0 +1,24 @@ +package xyz.bluspring.kilt.mixin.compat.fabric_api; + +import net.fabricmc.fabric.impl.resource.loader.ServerLanguageUtil; +import net.fabricmc.loader.api.ModContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.util.Collections; +import java.util.List; + +@Mixin(value = ServerLanguageUtil.class, remap = false) +public class ServerLanguageUtilMixin { + @Redirect(method = "getModLanguageFiles", at = @At(value = "INVOKE", target = "Lnet/fabricmc/loader/api/ModContainer;getRootPaths()Ljava/util/List;")) + private static List kilt$skipMissingJarFileSystems(ModContainer instance) { + try { + return instance.getRootPaths(); + } catch (ProviderNotFoundException exception) { + return Collections.emptyList(); + } + } +} diff --git a/src/main/kotlin/xyz/bluspring/kilt/loader/KiltLoader.kt b/src/main/kotlin/xyz/bluspring/kilt/loader/KiltLoader.kt index 7545b757e..9b6276120 100644 --- a/src/main/kotlin/xyz/bluspring/kilt/loader/KiltLoader.kt +++ b/src/main/kotlin/xyz/bluspring/kilt/loader/KiltLoader.kt @@ -420,7 +420,7 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { if (url.file.contains(".jar") && url.file.contains("!/META-INF/mods.toml")) // Prevent Fabric mods with broken Forge metadata from loading in the dev env continue - val path = Path(url.path.removePrefix("file:/")) + val path = url.toURI().toPath() val toml = tomlParser.parse(url) modsList.addAll(parseModsToml(path, toml, null, isBuiltIn = true)) @@ -524,6 +524,7 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { launch(Dispatchers.Default) { // Load Forge scan data immediately, then we can assign it when we need to. val forgeScanData = ModFileScanData() + val forgeScanDataLock = Any() KiltHelper.getForgeClassNodes().asFlow().collect { val visitor = ModClassVisitor() @@ -543,7 +544,9 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { if (mod.modFile == null) { // This is usually a Forge built-in, we don't have to worry about scanning this. // If it is in fact a built-in, let's assign the scan data. if (mod.definition.isBuiltin) { - forgeScanData.addModFileInfo(ModFileInfo(mod)) + synchronized(forgeScanDataLock) { + forgeScanData.addModFileInfo(ModFileInfo(mod)) + } mod.scanData = forgeScanData } @@ -670,6 +673,13 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { Kilt.logger.debug("Automatically registered event ${annotation.clazz.className} from mod ID $modId under bus ${busType.name}") } catch (e: Throwable) { + ModLoadingContext.kiltActiveModId = null + + if (isEnvironmentClassLoadingIssue(e)) { + Kilt.logger.debug("Skipping automatic event registration for ${annotation.clazz.className} due to environment mismatch.") + return@collect + } + Kilt.logger.error("Failed to register event ${annotation.clazz.className} from mod ${mod.modId}!") val ex = RuntimeException("Failed to register event ${annotation.clazz.className} from mod ${mod.modId}!", e) ex.printStackTrace() @@ -785,6 +795,15 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { ModLoadingContext.kiltActiveModId = null } catch (e: Throwable) { + ModLoadingContext.kiltActiveModId = null + + val envMismatch = isEnvironmentClassLoadingIssue(e) || isEnvironmentClassLoadingIssue(extraThrowable) + if (envMismatch) { + Kilt.logger.debug("Skipping mod class ${it.clazz.className} for ${mod.modId} due to environment mismatch.") + hasErrored = true + return@collect + } + e.printStackTrace() if (extraThrowable != null) exception.addSuppressed(extraThrowable) @@ -802,6 +821,23 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { } } + private fun isEnvironmentClassLoadingIssue(throwable: Throwable?): Boolean { + var current = throwable + while (current != null) { + val message = current.message + if (message != null && + message.startsWith("Cannot load class ") && + (message.contains("in environment type SERVER") || message.contains("in environment type CLIENT")) + ) { + return true + } + + current = current.cause + } + + return false + } + private fun loadTransformers(mod: ForgeMod) { if (mod.modFile == null || mod.definition.isBuiltin) { val accessTransformer = KiltLoader::class.java.getResource("META-INF/accesstransformer.cfg") @@ -935,4 +971,4 @@ class KiltLoader : KnitModLoader(Kilt.MOD_ID, "Forge") { runCatching { this.createDirectories() } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/xyz/bluspring/kilt/loader/remap/KiltRemapper.kt b/src/main/kotlin/xyz/bluspring/kilt/loader/remap/KiltRemapper.kt index 04a21222e..d9f53422a 100644 --- a/src/main/kotlin/xyz/bluspring/kilt/loader/remap/KiltRemapper.kt +++ b/src/main/kotlin/xyz/bluspring/kilt/loader/remap/KiltRemapper.kt @@ -46,7 +46,10 @@ import xyz.bluspring.kilt.util.KiltHelper import xyz.bluspring.knit.loader.mod.ModDefinition import xyz.bluspring.knit.loader.util.* import java.io.File +import java.net.URI import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.ProviderNotFoundException import java.nio.file.StandardOpenOption import java.util.* import java.util.function.Consumer @@ -90,6 +93,57 @@ object KiltRemapper { else FabricLoader.getInstance().mappingResolver + private fun ClassProvider.Builder.addLibrarySafe(path: Path) { + val isDirectory = runCatching { path.isDirectory() }.getOrDefault(false) + if (!isDirectory) { + val isRegularFile = runCatching { path.isRegularFile() }.getOrDefault(false) + if (!isRegularFile) { + return + } + + val name = path.fileName?.toString()?.lowercase(Locale.ROOT) + val isArchive = name?.endsWith(".jar") == true || name?.endsWith(".zip") == true || name?.endsWith(".jmod") == true + if (!isArchive) { + return + } + } + + try { + addLibrary(path) + return + } catch (_: ProviderNotFoundException) { + // Some classpath entries are backed by non-default file systems in dev runs. + // Normalize them to a physical path that AutoRenamingTool can open. + } + + val fallbackPath = runCatching { + val uri = path.toUri() + when (uri.scheme) { + "jar" -> { + val nested = uri.toString().removePrefix("jar:").substringBefore("!/") + Paths.get(URI(nested)) + } + "file" -> Paths.get(uri) + else -> null + } + }.getOrNull() + + if (fallbackPath != null) { + try { + addLibrary(fallbackPath) + return + } catch (exception: ProviderNotFoundException) { + throw RuntimeException( + "Failed to add class library path '$path' (uri=${runCatching { path.toUri() }.getOrNull()}) " + + "using fallback '$fallbackPath' (uri=${runCatching { fallbackPath.toUri() }.getOrNull()})", + exception + ) + } + } + + throw RuntimeException("Could not normalize class library path '$path' (uri=${runCatching { path.toUri() }.getOrNull()}).") + } + // This is created automatically using https://github.com/BluSpring/srg2intermediary // srg -> intermediary val srgIntermediaryMapping: IMappingFile = this::class.java.getResourceAsStream("/srg_intermediary.tiny")!!.buffered().use { IMappingFile.load(it) } @@ -284,7 +338,7 @@ object KiltRemapper { // in order to use mods lmao *modLoadingQueue.map { mod -> mod.path }.toTypedArray() ).forEach { - addLibrary(it) + addLibrarySafe(it) } }.build() @@ -298,7 +352,7 @@ object KiltRemapper { ClassProvider.builder().apply { // time to add Intermediary mappings to the mix! :,D if (FabricLoader.getInstance().isDevelopmentEnvironment && !forceProductionRemap) { - addLibrary(intermediaryMap) + intermediaryMap?.let { addLibrarySafe(it) } } // IMPORTANT: this cannot be a flow or use merge, otherwise the order isn't retained. srgGamePath MUST be at the top of the list. @@ -314,7 +368,7 @@ object KiltRemapper { // in order to use mods lmao *modLoadingQueue.map { mod -> mod.path }.toTypedArray() ).forEach { - addLibrary(it) + addLibrarySafe(it) } }.build() } @@ -717,9 +771,9 @@ object KiltRemapper { val startTime = System.currentTimeMillis() val classProvider = ClassProvider.builder().apply { - this.addLibrary(gameFile) + this.addLibrarySafe(gameFile) for (path in getGameClassPath()) { - this.addLibrary(path) + this.addLibrarySafe(path) } }.build() val srgRemapper = EnhancedRemapper(classProvider, mappingFile, logConsumer) diff --git a/src/main/resources/Kilt.mixins.json b/src/main/resources/Kilt.mixins.json index 709530d02..ad57b10c1 100644 --- a/src/main/resources/Kilt.mixins.json +++ b/src/main/resources/Kilt.mixins.json @@ -67,7 +67,9 @@ "compat.fabric_api.BlockInitTrackerMixin", "compat.fabric_api.FabricItemMixin", "compat.fabric_api.Int2ObjectMapTrackerMixin", + "compat.fabric_api.ModNioResourcePackMixin", "compat.fabric_api.PackRepositoryMixin", + "compat.fabric_api.ServerLanguageUtilMixin", "compat.forge.alexscaves.PotionUtilsMixin", "compat.forge.blueprint.RemoldedResourceManagerMixin", "compat.forge.decocraft.JsonParserMixin",