From 71dd78cae16357d476948f921da729f033133bdf Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 14 Nov 2025 00:30:34 +0100 Subject: [PATCH] feat: Add mod id support to the deploader --- build.gradle.kts | 5 +- .../com/falsepattern/deploader/DepRoot.java | 84 ++++++++++++++- .../deploader/DependencyLoaderImpl.java | 102 +++++++++++++++--- src/main/resources/DEPENDENCIES.md | 11 +- 4 files changed, 179 insertions(+), 23 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ac8306f..151a025 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,9 @@ plugins { group = "com.falsepattern" +//bump this after ANY change to the deploader! +val deploaderVersion = 2 + minecraft_fp { java { compatibility = jvmDowngrader @@ -95,7 +98,7 @@ val depLoaderJar = tasks.register(depLoader.jarTaskName) { archiveVersion = minecraft_fp.mod.version archiveClassifier = "deploader" manifest { - attributes("FPLib-Deploader-Version" to "1") + attributes("FPLib-Deploader-Version" to deploaderVersion) } } diff --git a/src/deploader/java/com/falsepattern/deploader/DepRoot.java b/src/deploader/java/com/falsepattern/deploader/DepRoot.java index 6d196cb..156901f 100644 --- a/src/deploader/java/com/falsepattern/deploader/DepRoot.java +++ b/src/deploader/java/com/falsepattern/deploader/DepRoot.java @@ -22,10 +22,21 @@ package com.falsepattern.deploader; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.Expose; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; +import lombok.val; +import org.jetbrains.annotations.Nullable; +import java.io.IOException; import java.util.List; @Data @@ -37,7 +48,7 @@ public class DepRoot { @Expose private Integer maxJava; @Expose - private List bundledArtifacts; + private List bundledArtifacts; @Expose private List repositories; @Expose @@ -60,10 +71,75 @@ public class DepRoot { @Accessors(fluent = true) public static class SidedDependencies { @Expose - private List common; + private List common; @Expose - private List client; + private List client; @Expose - private List server; + private List server; + } + + @Data + @Accessors(fluent = true) + public static class Dependency { + @Expose + private @Nullable String modid; + @Expose + private String artifact; + + @RequiredArgsConstructor + public static class Adapter extends TypeAdapter { + private final TypeAdapter jsonElementTypeAdapter; + + @Override + public void write(JsonWriter out, Dependency value) throws IOException { + if (value.modid() == null) { + out.value(value.artifact()); + return; + } + out.beginObject(); + out.name("modid"); + out.value(value.modid()); + out.name("artifact"); + out.value(value.artifact()); + out.endObject(); + } + + @Override + public Dependency read(JsonReader in) throws IOException { + val dep = new Dependency(); + switch (in.peek()) { + case STRING: { + val artifact = in.nextString(); + dep.artifact(artifact); + break; + } + case BEGIN_OBJECT: { + val object = jsonElementTypeAdapter.read(in).getAsJsonObject(); + if (!object.has("artifact")) { + throw new IllegalArgumentException("Missing artifact in dependency object!"); + } + dep.artifact(object.get("artifact").getAsString()); + if (object.has("modid")) { + dep.modid(object.get("modid").getAsString()); + } + break; + } + } + return dep; + } + + public static class Factory implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + @Override + public @Nullable TypeAdapter create(Gson gson, TypeToken type) { + if (!Dependency.class.isAssignableFrom(type.getRawType())) { + return null; + } + val jsonElementAdapter = gson.getAdapter(JsonElement.class); + + return (TypeAdapter) new Adapter(jsonElementAdapter); + } + } + } } } diff --git a/src/deploader/java/com/falsepattern/deploader/DependencyLoaderImpl.java b/src/deploader/java/com/falsepattern/deploader/DependencyLoaderImpl.java index 5e42a50..ccc3cf6 100644 --- a/src/deploader/java/com/falsepattern/deploader/DependencyLoaderImpl.java +++ b/src/deploader/java/com/falsepattern/deploader/DependencyLoaderImpl.java @@ -39,6 +39,9 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import cpw.mods.fml.common.MetadataCollection; +import cpw.mods.fml.common.ModMetadata; + import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; @@ -53,6 +56,7 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Field; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; @@ -92,6 +96,8 @@ public class DependencyLoaderImpl { private static final Set alreadyScannedSources = new ConcurrentSet<>(); private static final Map loadedLibraries = new ConcurrentHashMap<>(); private static final Map loadedLibraryMods = new ConcurrentHashMap<>(); + private static final Map loadedModIds = new ConcurrentHashMap<>(); + private static final Map loadedModIdMods = new ConcurrentHashMap<>(); private static final Set remoteMavenRepositories = new ConcurrentSet<>(); private static final Set localMavenRepositories = new ConcurrentSet<>(); static final Logger LOG = LogManager.getLogger("FalsePatternLib DepLoader"); @@ -106,6 +112,16 @@ public class DependencyLoaderImpl { private static final Path libDir; private static final Path modsDir; private static final Path tempDir; + private static final Field metadataCollectionModListAccessor; + + static { + try { + metadataCollectionModListAccessor = MetadataCollection.class.getDeclaredField("modList"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + metadataCollectionModListAccessor.setAccessible(true); + } private static AtomicBoolean modDownloaded = new AtomicBoolean(false); @@ -260,7 +276,8 @@ public class DependencyLoaderImpl { library.preferredVersion, library.regularSuffix, library.devSuffix, - false); + false, + null); futures.add(CompletableFuture.supplyAsync(task::load, executor)); } return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(ignored -> { @@ -274,6 +291,26 @@ public class DependencyLoaderImpl { }); } + @SneakyThrows + private static void readMCMod(InputStream input, String name) { + val meta = MetadataCollection.from(input, name); + val modList = (ModMetadata[])metadataCollectionModListAccessor.get(meta); + for (val mod: modList) { + val id = mod.modId; + if (id != null) { + val versionStr = mod.version; + final Version version; + if (versionStr != null) { + version = Version.parse(versionStr); + } else { + version = new RawVersion("unknown"); + } + loadedModIds.put(id, version); + loadedModIdMods.put(id, name); + } + } + } + private static boolean scanForDepSpecs(URL source, List output, List jijURLs) { if (!source.getProtocol().equals("file")) { return false; @@ -298,6 +335,9 @@ public class DependencyLoaderImpl { ZipEntry entry; while ((entry = jarFile.getNextEntry()) != null) { val name = entry.getName(); + if (name.equals("mcmod.info")) { + readMCMod(jarFile, source.toString()); + } if (!name.startsWith("META-INF/")) continue; if (name.endsWith(".json") && name.matches("META-INF/\\w+.json")) { @@ -430,7 +470,12 @@ public class DependencyLoaderImpl { JOptionPane.showMessageDialog(null, "A minecraft mod has been downloaded by the FalsePatternLib dependency downloader.\nYou must restart the game to apply these changes.", "Mod Dependency Download notice", JOptionPane.WARNING_MESSAGE); } catch (Exception ignored) {} } - throw new ModDependencyDownloaded("A minecraft mod has been downloaded by the FalsePatternLib dependency downloader, and requires a game restart to get installed properly."); + val msg = "A minecraft mod has been downloaded by the FalsePatternLib dependency downloader, and requires a game restart to get installed properly."; + for (int i = 0; i < 16; i++) { + LOG.warn(msg); + } + System.exit(0); + throw new ModDependencyDownloaded(msg); } } @@ -495,6 +540,7 @@ public class DependencyLoaderImpl { } val builder = new GsonBuilder(); builder.excludeFieldsWithoutExposeAnnotation(); + builder.registerTypeAdapterFactory(new DepRoot.Dependency.Adapter.Factory()); val gson = builder.create(); json.remove("identifier"); val root = gson.fromJson(json, DepRoot.class); @@ -524,8 +570,8 @@ public class DependencyLoaderImpl { if (bundled == null) { continue; } - for (String artifact : bundled) { - val parts = artifact.split(":"); + for (var dep : bundled) { + val parts = dep.artifact().split(":"); if (parts.length < 3) { LOG.error("Invalid bundled artifact: {}", dependencySpec); continue; @@ -549,11 +595,22 @@ public class DependencyLoaderImpl { dependencySpec.source()); } val id = groupId + ":" + artifactId + ":" + classifier; + val src = dependencySpec.source(); if (!loadedLibraries.containsKey(id)) { loadedLibraries.put(id, version); } if (!loadedLibraryMods.containsKey(id)) { - loadedLibraryMods.put(id, dependencySpec.source()); + loadedLibraryMods.put(id, src); + } + val modid = dep.modid(); + if (modid != null) { + LOG.info("With modid: {}", modid); + if (!loadedModIds.containsKey(modid)) { + loadedModIds.put(modid, version); + } + if (!loadedModIdMods.containsKey(modid)) { + loadedModIdMods.put(modid, src); + } } } } @@ -578,7 +635,7 @@ public class DependencyLoaderImpl { val source = scopedSidedDep.source; val scope = scopedSidedDep.scope; val dep = scopedSidedDep.dep; - val parts = dep.split(":"); + val parts = dep.artifact().split(":"); if (parts.length < 3) { LOG.error("Invalid dependency: {}", dep); return null; @@ -610,7 +667,8 @@ public class DependencyLoaderImpl { version, classifier, classifier, - scopedSidedDep.mod)); + scopedSidedDep.mod, + dep.modid())); }) .filter(Objects::nonNull) .collect(Collectors.toSet()); @@ -620,7 +678,7 @@ public class DependencyLoaderImpl { return artifacts; } - private static Stream concat(ScopedDep it, @Nullable Stream prev, List deps, DependencySide side) { + private static Stream concat(ScopedDep it, @Nullable Stream prev, List deps, DependencySide side) { if (deps != null) { val newStream = deps.stream().map(dep -> new ScopedSidedDep(it.source, it.mod, new ScopeSide(it.scope, side), dep)); if (prev == null) { @@ -826,7 +884,7 @@ public class DependencyLoaderImpl { public final String source; public final boolean mod; public final ScopeSide scope; - public final String dep; + public final DepRoot.Dependency dep; } @RequiredArgsConstructor @@ -851,6 +909,7 @@ public class DependencyLoaderImpl { private final String regularSuffix; private final String devSuffix; private final boolean isMod; + private final String modId; private String suffix; private String artifactLogName; @@ -865,7 +924,11 @@ public class DependencyLoaderImpl { private @Nullable URL load() { setupLibraryNames(); if (loadedLibraries.containsKey(artifact)) { - alreadyLoaded(); + alreadyLoaded(false); + return null; + } + if (isMod && loadedModIds.containsKey(modId)) { + alreadyLoaded(true); return null; } setupPaths(); @@ -909,14 +972,14 @@ public class DependencyLoaderImpl { artifact = groupId + ":" + artifactId + ":" + suffix; } - private void alreadyLoaded() { - val currentVer = loadedLibraries.get(artifact); + private void alreadyLoaded(boolean fromModId) { + val currentVer = fromModId ? loadedModIds.get(modId) : loadedLibraries.get(artifact); if (currentVer.equals(preferredVersion)) { return; } val rangeString = "(minimum: " + minVersion + (maxVersion == null ? "" : ", maximum: " + maxVersion) + ")"; if (minVersion.compareTo(currentVer) > 0 || (maxVersion != null && maxVersion.compareTo(currentVer) < 0)) { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < 4; i++) { LOG.fatal("ALERT VVVVVVVVVVVV ALERT"); } LOG.fatal("Library {}:{}{} already loaded with version {}, " @@ -928,8 +991,8 @@ public class DependencyLoaderImpl { currentVer, rangeString, loadingModId, - loadedLibraryMods.get(artifact)); - for (int i = 0; i < 16; i++) { + fromModId ? loadedModIdMods.get(modId) : loadedLibraryMods.get(artifact)); + for (int i = 0; i < 4; i++) { LOG.fatal("ALERT ^^^^^^^^^^^^ ALERT"); } } else { @@ -944,7 +1007,7 @@ public class DependencyLoaderImpl { currentVer, rangeString, loadingModId, - loadedLibraryMods.get(artifact)); + fromModId ? loadedModIdMods.get(modId) : loadedLibraryMods.get(artifact)); } } @@ -978,6 +1041,9 @@ public class DependencyLoaderImpl { addToClasspath(theUrl); } loadedLibraries.put(artifact, preferredVersion); + if (isMod && modId != null) { + loadedModIds.put(modId, preferredVersion); + } LOG.debug("Library {} successfully loaded from disk!", artifactLogName); return theUrl; } catch (Exception e) { @@ -1075,6 +1141,10 @@ public class DependencyLoaderImpl { if (!isMod) { addToClasspath(diskUrl); } + if (isMod && modId != null) { + loadedModIds.put(modId, preferredVersion); + loadedModIdMods.put(modId, loadingModId); + } return diskUrl; } } diff --git a/src/main/resources/DEPENDENCIES.md b/src/main/resources/DEPENDENCIES.md index a37bcac..95711ed 100644 --- a/src/main/resources/DEPENDENCIES.md +++ b/src/main/resources/DEPENDENCIES.md @@ -37,7 +37,11 @@ DependencyLoader api. This file needs to reside inside the META-INF directory. "modDependencies": { "always": { "common": [ - "com.example:mymod:3.0.0" + "com.example:mymod:3.0.0", + { + "modid": "mylib", + "artifact": "com.example:mylib:1.0.2" + } ], "client": [], "server": [] @@ -81,7 +85,10 @@ Explanation: - The `dev` category gets downloaded only in the dev environment. Usually not needed, as gradle will automatically download dependencies. - `modDependencies`: Same as `dependencies`, but files from here get downloaded into `.minecraft/mods/1.7.10`. Useful - if you want to download third party mods as well. + if you want to download third party mods as well. Additionally, instead of a single maven artifact string, you can use + a json object with separate `modid` and `artifact` attributes, which can be used to filter against a specific mod ID in + addition to plain artifact matching against other downloaded libraries and `bundledArtifacts`. This is useful if you + want to depload a mod that doesn't have a marker json in it. Each of these categories also have 3 more subcategories: