From c64f9304fa1a6ebf8798ca4ecc0dd9b645ffef4a Mon Sep 17 00:00:00 2001 From: Zefir Date: Sun, 11 Oct 2026 11:17:37 +0200 Subject: [PATCH 1/4] Fix wrong paths --- .../java/dev/aoqia/leaf/proxy/ProxyPaths.java | 69 +++++++++++++++ src/main/java/dev/aoqia/leaf/proxy/Utils.java | 10 ++- .../proxy/entrypoints/ClientEntrypoint.java | 88 ++++++++----------- 3 files changed, 114 insertions(+), 53 deletions(-) create mode 100644 src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java diff --git a/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java new file mode 100644 index 0000000..16344b8 --- /dev/null +++ b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java @@ -0,0 +1,69 @@ +package dev.aoqia.leaf.proxy; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +public final class ProxyPaths { + private static final List GAME_JAR_PATHS = List.of( + "projectzomboid.jar", "projectzomboid/projectzomboid.jar"); + + public static Path gameJar(final Path proxyJar, final Path workingDirectory) throws IOException { + for (Path directory = proxyJar.toAbsolutePath().normalize().getParent(); directory != null; + directory = directory.getParent()) { + for (String relativePath : GAME_JAR_PATHS) { + final Path candidate = directory.resolve(relativePath); + if (Files.isRegularFile(candidate)) { + return candidate.toRealPath(); + } + } + } + final Path candidate = workingDirectory.toAbsolutePath().normalize().resolve("projectzomboid.jar"); + if (Files.isRegularFile(candidate)) { + return candidate.toRealPath(); + } + throw new IOException("Cannot find projectzomboid.jar relative to " + proxyJar + + " or in " + workingDirectory.toAbsolutePath()); + } + + public static Path loaderDirectory(final Path gameJar, final String workshopId, final String override) + throws IOException { + if (override != null) { + if (override.isBlank()) { + throw new IOException("-loaderPath must name the directory containing the loader and dependency JARs"); + } + return Path.of(override).toAbsolutePath().normalize(); + } + for (Path directory = gameJar.getParent(); directory != null; directory = directory.getParent()) { + if (directory.getFileName() != null + && directory.getFileName().toString().equalsIgnoreCase("steamapps")) { + return directory.resolve("workshop/content/108600").resolve(workshopId) + .resolve("mods/LeafLoader/common/media/java"); + } + } + throw new IOException("Cannot locate Steam Workshop relative to " + gameJar + + ". Use the proxy argument -loaderPath=."); + } + + public static List loaderJars(final Path directory) throws IOException { + if (!Files.isDirectory(directory)) { + throw new IOException("Loader directory does not exist: " + directory + + ". Subscribe to Leaf Loader (Workshop 3776625738), finish its download, or set -loaderPath."); + } + try (var files = Files.list(directory)) { + final List jars = files.filter(Files::isRegularFile) + .filter(path -> { + final String name = path.getFileName().toString(); + return name.endsWith(".jar") && !name.endsWith("-sources.jar"); + }) + .sorted(Comparator.comparing(path -> path.getFileName().toString())) + .toList(); + if (jars.isEmpty()) { + throw new IOException("No loader JARs found in " + directory); + } + return jars; + } + } +} diff --git a/src/main/java/dev/aoqia/leaf/proxy/Utils.java b/src/main/java/dev/aoqia/leaf/proxy/Utils.java index 037995e..f1b4478 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/Utils.java +++ b/src/main/java/dev/aoqia/leaf/proxy/Utils.java @@ -22,7 +22,7 @@ public class Utils { public static void initLog(final File gameDir) throws IOException { File leafDir = new File(gameDir, ".leaf/"); if (!leafDir.exists()) { - Files.createDirectory(leafDir.toPath()); + Files.createDirectories(leafDir.toPath()); } File logFile = new File(leafDir, "proxy.log"); @@ -40,12 +40,16 @@ public static void closeLog() { public static void log(final String s) { System.out.println(s); - LOG.println(s); + if (LOG != null) { + LOG.println(s); + } } public static void log(final Exception e) { e.printStackTrace(System.err); - e.printStackTrace(LOG); + if (LOG != null) { + e.printStackTrace(LOG); + } } public static byte[] remapClass(final byte[] originalBytes, final String newInternalName) { diff --git a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java index 017f2a0..db94f77 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java +++ b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java @@ -16,6 +16,7 @@ import dev.aoqia.leaf.proxy.Constants; import dev.aoqia.leaf.proxy.Main; +import dev.aoqia.leaf.proxy.ProxyPaths; import dev.aoqia.leaf.proxy.Utils; import dev.aoqia.leaf.proxy.json.InstallerJson; @@ -27,20 +28,28 @@ public class ClientEntrypoint { static void main(final String[] args) { try { final File proxyJar = getProxyJarPath(); - Utils.initLog(proxyJar.getParentFile()); + final Path gameJar = ProxyPaths.gameJar(proxyJar.toPath(), Path.of("")); + Utils.initLog(gameJar.getParent().toFile()); log("Initialised logging for leaf-loader-proxy."); log("Proxy args: " + Arrays.toString(Main.args.toArray())); - final File gameJar = proxyJar.toPath().getParent().resolve("projectzomboid.jar").toFile(); + log("Proxy JAR: " + proxyJar); + log("Game JAR: " + gameJar); + if (System.getProperty("leaf.gameJarPath") == null + && System.getProperty("leaf.gameJarPath.client") == null) { + System.setProperty("leaf.gameJarPath", gameJar.toString()); + } + log("Leaf common game JAR: " + System.getProperty("leaf.gameJarPath")); + log("Leaf client game JAR: " + System.getProperty("leaf.gameJarPath.client")); // If there's a better way to not hardcode this ID... - final File[] loaderJars = getLoaderJars(proxyJar, "3776625738"); + final File[] loaderJars = getLoaderJars(gameJar, "3776625738"); final URL[] urls = new URL[loaderJars.length + 1]; for (int i = 0; i < loaderJars.length; ++i) { urls[i] = loaderJars[i].toURI().toURL(); } - urls[urls.length - 1] = gameJar.toURI().toURL(); + urls[urls.length - 1] = gameJar.toUri().toURL(); // Remove the proxy jar from the classpath ASAP Utils.removeFromClasspath(s -> s.endsWith(proxyJar.getName())); @@ -54,22 +63,28 @@ static void main(final String[] args) { }).toArray(String[]::new)); log("Classpath is currently: " + System.getProperty(Constants.Properties.CLASS_PATH)); - try (final URLClassLoader cl = new URLClassLoader(urls, - ClientEntrypoint.class.getClassLoader().getParent())) { - log("Classloader was created successfully."); - - final Class entrypoint = Class.forName(getLoaderEntrypoint(cl), true, cl); - final Method entryMethod = entrypoint.getMethod("main", String[].class); - - log("Invoking loader entrypoint, have fun!"); + final URLClassLoader cl = new URLClassLoader(urls, + ClassLoader.getPlatformClassLoader()); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { - entryMethod.invoke(null, (Object) args); - } catch (InvocationTargetException e) { - log("Failed on thread " + Thread.currentThread().getName() + " with TCCL " - + Thread.currentThread().getContextClassLoader()); - log(e); - throw e; + cl.close(); + } catch (IOException e) { + e.printStackTrace(System.err); } + }, "leaf-proxy-classloader-close")); + log("Classloader was created successfully."); + + final Class entrypoint = Class.forName(getLoaderEntrypoint(cl), true, cl); + final Method entryMethod = entrypoint.getMethod("main", String[].class); + + log("Invoking loader entrypoint, have fun!"); + try { + entryMethod.invoke(null, (Object) args); + } catch (InvocationTargetException e) { + log("Failed on thread " + Thread.currentThread().getName() + " with TCCL " + + Thread.currentThread().getContextClassLoader()); + log(e); + throw e; } } catch (Exception e) { Utils.closeLog(); @@ -78,41 +93,14 @@ static void main(final String[] args) { } private static File getProxyJarPath() throws URISyntaxException { - return new File(ClientEntrypoint.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + return new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()); } - private static File[] getLoaderJars(final File proxyJar, final String workshopId) { + private static File[] getLoaderJars(final Path gameJar, final String workshopId) throws IOException { log("Attempting to get loader jars..."); - - final Path jarDir; - if (Main.args.containsKey("loaderPath")) { - jarDir = Path.of(Main.args.get("loaderPath")); - } else { - final Path workshopPath = getWorkshopPath(proxyJar); - final Path modPath = workshopPath.resolve(workshopId, "mods", "LeafLoader"); - jarDir = modPath.resolve("common", "media", "java"); - } - - final File[] jars = jarDir - .toFile() - .listFiles((_, name) -> name.endsWith(".jar") && !name.endsWith("-sources.jar")); - - if (jars == null || jars.length == 0) { - final var e = new RuntimeException("Loader JAR doesn't exist in the workshop mod"); - log(e); - throw e; - } - - return jars; - } - - private static Path getWorkshopPath(final File proxyJar) { - Path gamePath = proxyJar.toPath().getParent(); - if ("projectzomboid".equals(gamePath.getFileName().toString())) { - gamePath = gamePath.getParent(); - } - - return gamePath.getParent().getParent().resolve("workshop", "content", "108600"); + final Path jarDir = ProxyPaths.loaderDirectory(gameJar, workshopId, Main.args.get("loaderPath")); + log("Loader directory: " + jarDir); + return ProxyPaths.loaderJars(jarDir).stream().map(Path::toFile).toArray(File[]::new); } private static String getLoaderEntrypoint(final URLClassLoader cl) throws IOException { From ef6f31368893681b90af52d6d8217910c8a58e98 Mon Sep 17 00:00:00 2001 From: Zefir Date: Sun, 11 Oct 2026 11:29:03 +0200 Subject: [PATCH 2/4] feels better --- .../java/dev/aoqia/leaf/proxy/ProxyPaths.java | 15 ++++++++++++--- src/main/java/dev/aoqia/leaf/proxy/Utils.java | 10 ++++++---- .../leaf/proxy/entrypoints/ClientEntrypoint.java | 7 ++++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java index 16344b8..9e7f6b9 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java +++ b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java @@ -1,5 +1,7 @@ package dev.aoqia.leaf.proxy; +import dev.aoqia.leaf.proxy.entrypoints.ClientEntrypoint; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -20,15 +22,17 @@ public static Path gameJar(final Path proxyJar, final Path workingDirectory) thr } } } + final Path candidate = workingDirectory.toAbsolutePath().normalize().resolve("projectzomboid.jar"); if (Files.isRegularFile(candidate)) { return candidate.toRealPath(); } + throw new IOException("Cannot find projectzomboid.jar relative to " + proxyJar + " or in " + workingDirectory.toAbsolutePath()); } - public static Path loaderDirectory(final Path gameJar, final String workshopId, final String override) + public static Path loaderDirectory(final Path gameJar, final String override) throws IOException { if (override != null) { if (override.isBlank()) { @@ -36,13 +40,15 @@ public static Path loaderDirectory(final Path gameJar, final String workshopId, } return Path.of(override).toAbsolutePath().normalize(); } + for (Path directory = gameJar.getParent(); directory != null; directory = directory.getParent()) { if (directory.getFileName() != null && directory.getFileName().toString().equalsIgnoreCase("steamapps")) { - return directory.resolve("workshop/content/108600").resolve(workshopId) + return directory.resolve("workshop/content/108600").resolve(ClientEntrypoint.WORKSHOP_ID) .resolve("mods/LeafLoader/common/media/java"); } } + throw new IOException("Cannot locate Steam Workshop relative to " + gameJar + ". Use the proxy argument -loaderPath=."); } @@ -50,8 +56,9 @@ public static Path loaderDirectory(final Path gameJar, final String workshopId, public static List loaderJars(final Path directory) throws IOException { if (!Files.isDirectory(directory)) { throw new IOException("Loader directory does not exist: " + directory - + ". Subscribe to Leaf Loader (Workshop 3776625738), finish its download, or set -loaderPath."); + + ". Subscribe to Leaf Loader (Workshop " + ClientEntrypoint.WORKSHOP_ID + "), finish its download, or set -loaderPath."); } + try (var files = Files.list(directory)) { final List jars = files.filter(Files::isRegularFile) .filter(path -> { @@ -60,9 +67,11 @@ public static List loaderJars(final Path directory) throws IOException { }) .sorted(Comparator.comparing(path -> path.getFileName().toString())) .toList(); + if (jars.isEmpty()) { throw new IOException("No loader JARs found in " + directory); } + return jars; } } diff --git a/src/main/java/dev/aoqia/leaf/proxy/Utils.java b/src/main/java/dev/aoqia/leaf/proxy/Utils.java index f1b4478..59b0620 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/Utils.java +++ b/src/main/java/dev/aoqia/leaf/proxy/Utils.java @@ -40,16 +40,18 @@ public static void closeLog() { public static void log(final String s) { System.out.println(s); - if (LOG != null) { - LOG.println(s); + if (LOG == null) { + return; } + LOG.println(s); } public static void log(final Exception e) { e.printStackTrace(System.err); - if (LOG != null) { - e.printStackTrace(LOG); + if (LOG == null) { + return; } + e.printStackTrace(LOG); } public static byte[] remapClass(final byte[] originalBytes, final String newInternalName) { diff --git a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java index db94f77..64e7edb 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java +++ b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java @@ -24,6 +24,7 @@ public class ClientEntrypoint { private static final DslJson JSON = new DslJson<>(Settings.basicSetup()); + public static final String WORKSHOP_ID = "3776625738"; static void main(final String[] args) { try { @@ -43,7 +44,7 @@ static void main(final String[] args) { log("Leaf client game JAR: " + System.getProperty("leaf.gameJarPath.client")); // If there's a better way to not hardcode this ID... - final File[] loaderJars = getLoaderJars(gameJar, "3776625738"); + final File[] loaderJars = getLoaderJars(gameJar); final URL[] urls = new URL[loaderJars.length + 1]; for (int i = 0; i < loaderJars.length; ++i) { @@ -96,9 +97,9 @@ private static File getProxyJarPath() throws URISyntaxException { return new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()); } - private static File[] getLoaderJars(final Path gameJar, final String workshopId) throws IOException { + private static File[] getLoaderJars(final Path gameJar) throws IOException { log("Attempting to get loader jars..."); - final Path jarDir = ProxyPaths.loaderDirectory(gameJar, workshopId, Main.args.get("loaderPath")); + final Path jarDir = ProxyPaths.loaderDirectory(gameJar, Main.args.get("loaderPath")); log("Loader directory: " + jarDir); return ProxyPaths.loaderJars(jarDir).stream().map(Path::toFile).toArray(File[]::new); } From a3b16872c41e044308a940747966455769898e93 Mon Sep 17 00:00:00 2001 From: Zefir Date: Sun, 11 Oct 2026 12:03:30 +0200 Subject: [PATCH 3/4] don't need anymore --- .../proxy/entrypoints/ClientEntrypoint.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java index 64e7edb..1636206 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java +++ b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java @@ -64,28 +64,22 @@ static void main(final String[] args) { }).toArray(String[]::new)); log("Classpath is currently: " + System.getProperty(Constants.Properties.CLASS_PATH)); - final URLClassLoader cl = new URLClassLoader(urls, - ClassLoader.getPlatformClassLoader()); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try (final URLClassLoader cl = new URLClassLoader(urls, + ClassLoader.getPlatformClassLoader())) { + log("Classloader was created successfully."); + + final Class entrypoint = Class.forName(getLoaderEntrypoint(cl), true, cl); + final Method entryMethod = entrypoint.getMethod("main", String[].class); + + log("Invoking loader entrypoint, have fun!"); try { - cl.close(); - } catch (IOException e) { - e.printStackTrace(System.err); + entryMethod.invoke(null, (Object) args); + } catch (InvocationTargetException e) { + log("Failed on thread " + Thread.currentThread().getName() + " with TCCL " + + Thread.currentThread().getContextClassLoader()); + log(e); + throw e; } - }, "leaf-proxy-classloader-close")); - log("Classloader was created successfully."); - - final Class entrypoint = Class.forName(getLoaderEntrypoint(cl), true, cl); - final Method entryMethod = entrypoint.getMethod("main", String[].class); - - log("Invoking loader entrypoint, have fun!"); - try { - entryMethod.invoke(null, (Object) args); - } catch (InvocationTargetException e) { - log("Failed on thread " + Thread.currentThread().getName() + " with TCCL " - + Thread.currentThread().getContextClassLoader()); - log(e); - throw e; } } catch (Exception e) { Utils.closeLog(); From 89695b7c02da44315ba1ddb479956dfebb360d70 Mon Sep 17 00:00:00 2001 From: Zefir Date: Wed, 14 Oct 2026 14:48:50 +0200 Subject: [PATCH 4/4] fix: address review feedback --- src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java | 7 ++++--- src/main/java/dev/aoqia/leaf/proxy/Utils.java | 6 ------ .../dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java | 7 ------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java index 9e7f6b9..71b0cb7 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java +++ b/src/main/java/dev/aoqia/leaf/proxy/ProxyPaths.java @@ -2,6 +2,7 @@ import dev.aoqia.leaf.proxy.entrypoints.ClientEntrypoint; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -10,7 +11,7 @@ public final class ProxyPaths { private static final List GAME_JAR_PATHS = List.of( - "projectzomboid.jar", "projectzomboid/projectzomboid.jar"); + "projectzomboid.jar", "projectzomboid/projectzomboid.jar", "Contents/Java/projectzomboid.jar"); public static Path gameJar(final Path proxyJar, final Path workingDirectory) throws IOException { for (Path directory = proxyJar.toAbsolutePath().normalize().getParent(); directory != null; @@ -28,7 +29,7 @@ public static Path gameJar(final Path proxyJar, final Path workingDirectory) thr return candidate.toRealPath(); } - throw new IOException("Cannot find projectzomboid.jar relative to " + proxyJar + throw new FileNotFoundException("Cannot find projectzomboid.jar relative to " + proxyJar + " or in " + workingDirectory.toAbsolutePath()); } @@ -69,7 +70,7 @@ public static List loaderJars(final Path directory) throws IOException { .toList(); if (jars.isEmpty()) { - throw new IOException("No loader JARs found in " + directory); + throw new RuntimeException("No loader JARs found in " + directory); } return jars; diff --git a/src/main/java/dev/aoqia/leaf/proxy/Utils.java b/src/main/java/dev/aoqia/leaf/proxy/Utils.java index 59b0620..17af93f 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/Utils.java +++ b/src/main/java/dev/aoqia/leaf/proxy/Utils.java @@ -40,17 +40,11 @@ public static void closeLog() { public static void log(final String s) { System.out.println(s); - if (LOG == null) { - return; - } LOG.println(s); } public static void log(final Exception e) { e.printStackTrace(System.err); - if (LOG == null) { - return; - } e.printStackTrace(LOG); } diff --git a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java index 1636206..d40b431 100644 --- a/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java +++ b/src/main/java/dev/aoqia/leaf/proxy/entrypoints/ClientEntrypoint.java @@ -33,15 +33,8 @@ static void main(final String[] args) { Utils.initLog(gameJar.getParent().toFile()); log("Initialised logging for leaf-loader-proxy."); log("Proxy args: " + Arrays.toString(Main.args.toArray())); - log("Proxy JAR: " + proxyJar); log("Game JAR: " + gameJar); - if (System.getProperty("leaf.gameJarPath") == null - && System.getProperty("leaf.gameJarPath.client") == null) { - System.setProperty("leaf.gameJarPath", gameJar.toString()); - } - log("Leaf common game JAR: " + System.getProperty("leaf.gameJarPath")); - log("Leaf client game JAR: " + System.getProperty("leaf.gameJarPath.client")); // If there's a better way to not hardcode this ID... final File[] loaderJars = getLoaderJars(gameJar);