From 172424a2532e78499097f8db13e521463f23b58d Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:04:29 +0100 Subject: [PATCH 1/4] ft missing mixin features --- gradlew | 0 .../java/cc/irori/hyinit/ConfigCollector.java | 52 +++++++------ src/main/java/cc/irori/hyinit/Main.java | 29 ++++++- .../irori/hyinit/mixin/HyinitClassLoader.java | 76 ++++++++++++++++--- .../hyinit/mixin/impl/MixinPluginManager.java | 14 ++-- 5 files changed, 125 insertions(+), 46 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/cc/irori/hyinit/ConfigCollector.java b/src/main/java/cc/irori/hyinit/ConfigCollector.java index a69f8df..a0b08c8 100644 --- a/src/main/java/cc/irori/hyinit/ConfigCollector.java +++ b/src/main/java/cc/irori/hyinit/ConfigCollector.java @@ -25,39 +25,45 @@ public final class ConfigCollector { private ConfigCollector() {} public static Result collectMixinConfigs(Path workingDir) { - Objects.requireNonNull(workingDir, "workingDir"); + return collectMixinConfigs(workingDir, List.of(workingDir.resolve("earlyplugins"))); + } - Path dir = workingDir.resolve("earlyplugins"); - if (!Files.isDirectory(dir)) { - return new Result(List.of(), Map.of(), List.of()); - } + public static Result collectMixinConfigs(Path workingDir, List earlyPluginDirs) { + Objects.requireNonNull(workingDir, "workingDir"); + Objects.requireNonNull(earlyPluginDirs, "earlyPluginDirs"); - List jars = listJars(dir); List warnings = new ArrayList<>(); Map origins = new LinkedHashMap<>(); - - // Preserve order, deduplicate LinkedHashSet configs = new LinkedHashSet<>(); - for (Path jar : jars) { - try (JarFile jf = new JarFile(jar.toFile(), false)) { - JarEntry entry = jf.getJarEntry("manifest.json"); - if (entry == null) continue; + for (Path dir : earlyPluginDirs) { + if (!Files.isDirectory(dir)) { + continue; + } + + List jars = listJars(dir); + + for (Path jar : jars) { + try (JarFile jf = new JarFile(jar.toFile(), false)) { + JarEntry entry = jf.getJarEntry("manifest.json"); + if (entry == null) continue; - JsonObject root = readJsonObject(jf, entry); - List found = extractMixinConfigs(root); + JsonObject root = readJsonObject(jf, entry); + List found = extractMixinConfigs(root); - for (String cfg : found) { - if (cfg == null) continue; - String normalized = normalizeConfigPath(cfg); - if (normalized.isEmpty()) continue; + for (String cfg : found) { + if (cfg == null) continue; + String normalized = normalizeConfigPath(cfg); + if (normalized.isEmpty()) continue; - configs.add(normalized); - origins.putIfAbsent(normalized, jar); + configs.add(normalized); + origins.putIfAbsent(normalized, jar); + } + } catch (Exception e) { + warnings.add("Failed to read " + jar.getFileName() + ": " + + e.getClass().getSimpleName() + + (e.getMessage() != null ? (": " + e.getMessage()) : "")); } - } catch (Exception e) { - warnings.add("Failed to read " + jar.getFileName() + ": " - + e.getClass().getSimpleName() + (e.getMessage() != null ? (": " + e.getMessage()) : "")); } } diff --git a/src/main/java/cc/irori/hyinit/Main.java b/src/main/java/cc/irori/hyinit/Main.java index e653113..b929e47 100644 --- a/src/main/java/cc/irori/hyinit/Main.java +++ b/src/main/java/cc/irori/hyinit/Main.java @@ -43,13 +43,19 @@ static void main(String[] args) throws Exception { .getLocation() .toURI()), new SourceMetadata(false)); - for (Path path : collectClasspathJars(serverJar, cwd.resolve("earlyplugins"))) { - classLoader.addCodeSource(path, new SourceMetadata(true)); + List earlyPluginDirs = new java.util.ArrayList<>(); + earlyPluginDirs.add(cwd.resolve("earlyplugins")); + earlyPluginDirs.addAll(parseEarlyPluginPaths(args)); + + for (Path dir : earlyPluginDirs) { + for (Path path : collectClasspathJars(serverJar, dir)) { + classLoader.addCodeSource(path, new SourceMetadata(true)); + } } HyinitMixinService.setGameClassLoader(classLoader); - ConfigCollector.Result result = ConfigCollector.collectMixinConfigs(cwd); + ConfigCollector.Result result = ConfigCollector.collectMixinConfigs(cwd, earlyPluginDirs); result.warnings().forEach(LOGGER::warn); List configs = result.configs(); @@ -112,6 +118,23 @@ private static List collectClasspathJars(Path serverJar, Path earlyPlugins return List.of(); } + private static List parseEarlyPluginPaths(String[] args) { + List paths = new java.util.ArrayList<>(); + for (int i = 0; i < args.length; i++) { + if (args[i].equals("--early-plugins") && i + 1 < args.length) { + for (String pathStr : args[i + 1].split(",")) { + paths.add(Paths.get(pathStr.trim())); + } + } else if (args[i].startsWith("--early-plugins=")) { + String value = args[i].substring("--early-plugins=".length()); + for (String pathStr : value.split(",")) { + paths.add(Paths.get(pathStr.trim())); + } + } + } + return paths; + } + private static void finishMixinBootstrapping() { try { Method m = MixinEnvironment.class.getDeclaredMethod("gotoPhase", MixinEnvironment.Phase.class); diff --git a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java index cef826c..3858157 100644 --- a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java +++ b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java @@ -1,22 +1,42 @@ package cc.irori.hyinit.mixin; -import cc.irori.hyinit.HyinitLogger; -import cc.irori.hyinit.shared.SourceMetaStore; -import cc.irori.hyinit.shared.SourceMetadata; -import cc.irori.hyinit.util.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.*; -import java.nio.file.*; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.CodeSource; import java.security.SecureClassLoader; import java.security.cert.Certificate; -import java.util.*; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.Manifest; + +import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.transformer.IMixinTransformer; +import cc.irori.hyinit.HyinitLogger; +import cc.irori.hyinit.shared.SourceMetaStore; +import cc.irori.hyinit.shared.SourceMetadata; +import cc.irori.hyinit.util.LoaderUtil; +import cc.irori.hyinit.util.ManifestUtil; +import cc.irori.hyinit.util.UrlConversionException; +import cc.irori.hyinit.util.UrlUtil; + public class HyinitClassLoader extends SecureClassLoader { private static final boolean DEBUG = System.getProperty("hyinit.debugClassLoader") != null; @@ -281,13 +301,30 @@ private byte[] getPostMixinClassByteArray(String name, boolean allowFromParent) return original; } + if (original != null) { + try { + return transformer.transformClassBytes(name, name, original); + } catch (Throwable t) { + String message = String.format("Mixin transformation of %s failed", name); + HyinitLogger.get().error(message, t); + throw new RuntimeException(message, t); + } + } + + // No class file on disk, proceed with Mixin's generateClass for synthetics try { - return transformer.transformClassBytes(name, name, original); + byte[] generated = transformer.generateClass(MixinEnvironment.getCurrentEnvironment(), name); + if (generated != null) { + if (DEBUG) { + HyinitLogger.get().info(String.format("Generated synthetic class: %s (%d bytes)", name, generated.length)); + } + return generated; + } } catch (Throwable t) { - String message = String.format("Mixin transformation of %s failed", name); - HyinitLogger.get().error(message, t); - throw new RuntimeException(message, t); + // fall through on no synthetic classes } + + return null; } public boolean isClassLoaded(String name) { @@ -352,8 +389,23 @@ private Metadata getMetadata(Path sourcePath) { }); } + private static final Set TRANSFORM_EXCLUSIONS = Set.of( + "java.", "javax.", "jdk.", "sun.", "com.sun.", + "org.objectweb.asm.", + "org.spongepowered.asm.", + "cc.irori.hyinit.mixin.", "cc.irori.hyinit.shared.", + "org.slf4j.", "org.apache.logging.", "ch.qos.logback.", + "com.google.gson.", "com.google.flogger.", + "org.bouncycastle.", + "com.hypixel.hytale.plugin.early."); + private static boolean canTransformClass(String name) { - return true; // Placeholder + for (String prefix : TRANSFORM_EXCLUSIONS) { + if (name.startsWith(prefix)) { + return false; + } + } + return true; } private static boolean hasRegularCodeSource(URL url) { diff --git a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java index 6e2f22a..315436c 100644 --- a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java +++ b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java @@ -1,7 +1,6 @@ package cc.irori.hyinit.mixin.impl; import cc.irori.hyinit.util.UrlUtil; -import com.hypixel.hytale.Main; import com.hypixel.hytale.server.core.plugin.PluginManager; import java.io.IOException; import java.net.JarURLConnection; @@ -23,8 +22,9 @@ public abstract class MixinPluginManager { value = "INVOKE", target = "Ljava/lang/ClassLoader;getResources(Ljava/lang/String;)Ljava/util/Enumeration;")) private Enumeration hyinit$redirectManifestResources(ClassLoader instance, String name) throws IOException { - Path hytaleJarPath = - UrlUtil.asPath(Main.class.getProtectionDomain().getCodeSource().getLocation()); + // allow server and other classpath plugins through + Path hyinitJarPath = UrlUtil.asPath( + cc.irori.hyinit.Main.class.getProtectionDomain().getCodeSource().getLocation()); List urls = new ArrayList<>(); Enumeration resources = instance.getResources(name); @@ -34,13 +34,11 @@ public abstract class MixinPluginManager { if (connection instanceof JarURLConnection jarConnection) { Path jarPath = UrlUtil.asPath(jarConnection.getJarFileURL()); - // TODO: Allow other classpath plugins to be loaded - This impl breaks classpath plugin loading - if (jarPath.equals(hytaleJarPath)) { - urls.add(url); + if (jarPath.equals(hyinitJarPath)) { + continue; } - } else { - urls.add(url); } + urls.add(url); } return Collections.enumeration(urls); } From 8035aafbd977ec475eebfac26aac861f55c95f9c Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:08:44 +0100 Subject: [PATCH 2/4] look for generated synthetic classes --- .../irori/hyinit/mixin/HyinitClassLoader.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java index 3858157..8b53c1c 100644 --- a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java +++ b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java @@ -239,11 +239,30 @@ private Class tryLoadClass(String name, boolean allowFromParent) throws Class } public byte[] getClassByteArray(String name, boolean runTransformers) throws IOException { + byte[] bytes; if (runTransformers) { - return getPreMixinClassBytes(name); + bytes = getPreMixinClassBytes(name); } else { - return getRawClassBytes(name); + bytes = getRawClassBytes(name); } + + if (bytes != null) { + return bytes; + } + + // No class file on disk, test for synthetic class generation for @Accessor and @Invoker + if (isTransformerInitialized()) { + try { + byte[] generated = transformer.generateClass(MixinEnvironment.getCurrentEnvironment(), name); + if (generated != null) { + return generated; + } + } catch (Throwable t) { + // Not a synthetic class — fall through + } + } + + return null; } public byte[] getRawClassBytes(String name) throws IOException { @@ -393,6 +412,7 @@ private Metadata getMetadata(Path sourcePath) { "java.", "javax.", "jdk.", "sun.", "com.sun.", "org.objectweb.asm.", "org.spongepowered.asm.", + "com.llamalad7.mixinextras.", "cc.irori.hyinit.mixin.", "cc.irori.hyinit.shared.", "org.slf4j.", "org.apache.logging.", "ch.qos.logback.", "com.google.gson.", "com.google.flogger.", From ba34a20deb823741da50c2f15f66ec372794dfb0 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:55:23 +0100 Subject: [PATCH 3/4] Fix leak in early plugin manifest and resource enumeration merging --- .../irori/hyinit/mixin/HyinitClassLoader.java | 18 +++++++++--------- .../hyinit/mixin/impl/MixinPluginManager.java | 8 ++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java index 8b53c1c..4121d80 100644 --- a/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java +++ b/src/main/java/cc/irori/hyinit/mixin/HyinitClassLoader.java @@ -134,11 +134,15 @@ public InputStream getResourceAsStream(String name) { public Enumeration getResources(String name) throws IOException { Objects.requireNonNull(name, "name"); - Enumeration resources = urlLoader.getResources(name); - if (!resources.hasMoreElements()) { - return originalLoader.getResources(name); - } - return resources; + Enumeration primary = urlLoader.getResources(name); + Enumeration fallback = originalLoader.getResources(name); + + if (!primary.hasMoreElements()) return fallback; + if (!fallback.hasMoreElements()) return primary; + + var merged = Collections.list(primary); + while (fallback.hasMoreElements()) merged.add(fallback.nextElement()); + return Collections.enumeration(merged); } @Override @@ -190,10 +194,6 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } private Class tryLoadClass(String name, boolean allowFromParent) throws ClassNotFoundException { - if (name.startsWith(".java")) { - return null; - } - if (!allowFromParent && !parentSourcedClasses.isEmpty()) { int pos = name.length(); diff --git a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java index 315436c..6482f51 100644 --- a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java +++ b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginManager.java @@ -1,5 +1,8 @@ package cc.irori.hyinit.mixin.impl; +import cc.irori.hyinit.shared.SourceMetaStore; +import cc.irori.hyinit.shared.SourceMetadata; +import cc.irori.hyinit.util.LoaderUtil; import cc.irori.hyinit.util.UrlUtil; import com.hypixel.hytale.server.core.plugin.PluginManager; import java.io.IOException; @@ -37,6 +40,11 @@ public abstract class MixinPluginManager { if (jarPath.equals(hyinitJarPath)) { continue; } + + SourceMetadata meta = SourceMetaStore.get(LoaderUtil.normalizeExistingPath(jarPath)); + if (meta != null && meta.isEarlyPlugin()) { + continue; + } } urls.add(url); } From 4811c6f8813679f8f41aade5f3255e0a225102f4 Mon Sep 17 00:00:00 2001 From: Xytronix <32957125+Xytronix@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:05:33 +0100 Subject: [PATCH 4/4] bump java version --- .../java/cc/irori/hyinit/HyinitLogger.java | 23 +++++++++++-------- .../hyinit/mixin/HyinitMixinService.java | 2 +- .../mixin/impl/MixinPluginClassLoader.java | 17 ++++---------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/java/cc/irori/hyinit/HyinitLogger.java b/src/main/java/cc/irori/hyinit/HyinitLogger.java index bcfad5a..e3df55b 100644 --- a/src/main/java/cc/irori/hyinit/HyinitLogger.java +++ b/src/main/java/cc/irori/hyinit/HyinitLogger.java @@ -162,19 +162,24 @@ public void log(Level level, String message, Throwable t) { @Override public T throwing(T t) { - hytaleLogger.atWarning().withCause(t).log(); + if (isHytaleLoggerAvailable()) { + hytaleLogger.atWarning().withCause(t).log(); + } else { + JAVA_LOGGER.warn("throwing", t); + } return t; } private boolean isHytaleLoggerAvailable() { - if (hytaleLogger == null) { - try { - hytaleLogger = HytaleLogger.get(LOGGER_NAME); - return true; - } catch (NoClassDefFoundError | Exception ignored) { - } - } - return false; + if (hytaleLogger != null) { + return true; + } + try { + hytaleLogger = HytaleLogger.get(LOGGER_NAME); + return hytaleLogger != null; + } catch (NoClassDefFoundError | Exception ignored) { + return false; + } } private static java.util.logging.Level toJavaLevel(Level level) { diff --git a/src/main/java/cc/irori/hyinit/mixin/HyinitMixinService.java b/src/main/java/cc/irori/hyinit/mixin/HyinitMixinService.java index cee3f4f..868f88d 100644 --- a/src/main/java/cc/irori/hyinit/mixin/HyinitMixinService.java +++ b/src/main/java/cc/irori/hyinit/mixin/HyinitMixinService.java @@ -209,7 +209,7 @@ public MixinEnvironment.CompatibilityLevel getMinCompatibilityLevel() { @Override public MixinEnvironment.CompatibilityLevel getMaxCompatibilityLevel() { - return MixinEnvironment.CompatibilityLevel.JAVA_22; + return MixinEnvironment.CompatibilityLevel.JAVA_25; } @Override diff --git a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginClassLoader.java b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginClassLoader.java index dced087..3fda136 100644 --- a/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginClassLoader.java +++ b/src/main/java/cc/irori/hyinit/mixin/impl/MixinPluginClassLoader.java @@ -10,8 +10,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSource; +import java.util.Collections; import java.util.Enumeration; -import java.util.NoSuchElementException; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -33,8 +33,9 @@ public MixinPluginClassLoader(URL[] urls, ClassLoader parent) { String name, boolean useBridge, CallbackInfoReturnable> cir, - @Local(name = "loadClass") Class loadClass) + @Local(ordinal = 0) Class loadClass) throws ClassNotFoundException { + if (loadClass == null) return; String fileName = LoaderUtil.getClassFileName(name); URL url = super.getResource(fileName); if (url != null) { @@ -77,17 +78,7 @@ public MixinPluginClassLoader(URL[] urls, ClassLoader parent) { private Enumeration hyinit$blockLoadingManifestResources(ClassLoader instance, String name) throws IOException { if (name.equalsIgnoreCase("manifest.json")) { - return new Enumeration<>() { - @Override - public boolean hasMoreElements() { - return false; - } - - @Override - public URL nextElement() { - throw new NoSuchElementException(); - } - }; + return Collections.emptyEnumeration(); } return instance.getResources(name); }