From 2184596bff89bbe180fd4a631b2cca95722a6711 Mon Sep 17 00:00:00 2001 From: Goldenfield192 <1437356849@qq.com> Date: Wed, 10 Jun 2026 10:05:52 +0800 Subject: [PATCH 1/2] feat: production environment loading --- .../cam72cam/mod/loader/UMCModContainer.java | 171 ++++++++++++++++++ .../cam72cam/mod/loader/UMCModParser.java | 90 +++++++++ .../feat/mod_loading/MixinJarDiscoverer.java | 40 ++++ .../mixins.feat.universalmodcore.json | 3 +- 4 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cam72cam/mod/loader/UMCModContainer.java create mode 100644 src/main/java/cam72cam/mod/loader/UMCModParser.java create mode 100644 src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinJarDiscoverer.java diff --git a/src/main/java/cam72cam/mod/loader/UMCModContainer.java b/src/main/java/cam72cam/mod/loader/UMCModContainer.java new file mode 100644 index 000000000..f243166b7 --- /dev/null +++ b/src/main/java/cam72cam/mod/loader/UMCModContainer.java @@ -0,0 +1,171 @@ +package cam72cam.mod.loader; + +import cam72cam.mod.ModCore; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.*; +import net.minecraftforge.fml.common.discovery.ModCandidate; +import net.minecraftforge.fml.common.event.FMLConstructionEvent; +import net.minecraftforge.fml.common.versioning.ArtifactVersion; +import net.minecraftforge.fml.common.versioning.InvalidVersionSpecificationException; +import net.minecraftforge.fml.common.versioning.VersionRange; +import org.apache.logging.log4j.message.FormattedMessage; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +public class UMCModContainer extends DummyModContainer { + ModCandidate candidate; + File source; + ModCore.Mod instance; + + String modId; + String displayName; + String mainClass; + String version; + List authors; + List description; + URL modURL; + String license; + Map dependencies; + + ModMetadata meta; + + private EventBus eventBus; + + @Override + public String getModId() { + return modId; + } + + @Override + public String getName() { + return displayName; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public File getSource() { + return source; + } + + @Override + public void setEnabledState(boolean enabled) { + //NO-OP as UMC mods cannot be disabled + } + + @Override + public List getDependencies() { + return new ArrayList<>(dependencies.values()); + } + + @Override + public void bindMetadata(MetadataCollection mc) { + meta = new ModMetadata(); + meta.modId = modId; + meta.name = displayName; + meta.version = version; + meta.autogenerated = false; + meta.authorList = authors; + meta.url = modURL == null ? null : modURL.toString(); + meta.description = String.join("\n", description); + meta.dependencies = new ArrayList<>(dependencies.values()); + } + + @Override + public ModMetadata getMetadata() { + return meta; + } + + @Override + public boolean registerBus(EventBus bus, LoadController controller) { + FMLLog.log.debug("Enabling mod {}", getModId()); + this.eventBus = bus; + eventBus.register(this); + return true; + } + + @Override + public boolean matches(Object mod) { + return mod instanceof ModCore.Mod && mod == instance; + } + + + @Subscribe + public void constructMod(FMLConstructionEvent event) { + ModClassLoader modClassLoader = event.getModClassLoader(); + try + { + modClassLoader.addFile(source); + } + catch (MalformedURLException e) + { + FormattedMessage message = new FormattedMessage("{} Failed to add file to classloader: {}", getModId(), source); + throw new LoaderException(message.getFormattedMessage(), e); + } + modClassLoader.clearNegativeCacheFor(candidate.getClassList()); + + //Only place I could think to add this... + MinecraftForge.preloadCrashClasses(event.getASMHarvestedData(), getModId(), candidate.getClassList()); + + Class clazz; + try + { + clazz = Class.forName(mainClass, true, modClassLoader); + instance = (ModCore.Mod) clazz.newInstance(); + ModCore.register(instance); + } + catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) + { + FormattedMessage message = new FormattedMessage("{} Failed load class: {}", getModId(), mainClass); + throw new LoaderException(message.getFormattedMessage(), e); + } + } + + @Override + public Object getMod() { + return instance; + } + + @Override + public String getDisplayVersion() { + return version; + } + + @Override + public VersionRange acceptableMinecraftVersionRange() { + try { + return VersionRange.createFromVersionSpec("[1.7.10, )"); + } catch (InvalidVersionSpecificationException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean shouldLoadInEnvironment() { + //TODO Side only + return true; + } + + @Override + public URL getUpdateUrl() { + return modURL; + } + + @Override + public int getClassVersion() { + return 52; //Compatible with Java8 + } + + @Override + public String toString() { + return String.format("UMC Mod %s:%s", getName(), getVersion()); + } +} diff --git a/src/main/java/cam72cam/mod/loader/UMCModParser.java b/src/main/java/cam72cam/mod/loader/UMCModParser.java new file mode 100644 index 000000000..fed53fa16 --- /dev/null +++ b/src/main/java/cam72cam/mod/loader/UMCModParser.java @@ -0,0 +1,90 @@ +package cam72cam.mod.loader; + +import cam72cam.mod.ModCore; +import com.google.gson.*; +import net.minecraftforge.fml.common.discovery.ModCandidate; +import net.minecraftforge.fml.common.versioning.ArtifactVersion; +import net.minecraftforge.fml.common.versioning.VersionParser; + +import java.io.*; +import java.net.URL; +import java.util.*; +import java.util.function.Supplier; + +public class UMCModParser { + private static final JsonParser parser = new JsonParser(); + + //TODO not only jar but also directory + public static UMCModContainer parse(ModCandidate candidate, InputStream stream) throws NoSuchElementException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + JsonElement root = parser.parse(reader); + JsonObject obj = root.getAsJsonObject(); + + UMCModContainer container = new UMCModContainer(); + + container.candidate = candidate; + container.source = candidate.getModContainer(); + container.modId = getString(obj, "modId").orElseThrow(form(candidate, "modId")); + container.displayName = getString(obj, "displayName").orElseThrow(form(candidate, "displayName")); + container.mainClass = getString(obj, "mainClass").orElseThrow(form(candidate, "mainClass")); + container.version = getString(obj, "version").orElseThrow(form(candidate, "version")); + String modURL = getString(obj, "modURL").orElse(""); + if (!modURL.isEmpty()) { + container.modURL = new URL(modURL); + } + container.license = getString(obj, "license").orElse("N/A"); + + container.authors = new ArrayList<>(); + if (obj.has("authors") && obj.get("authors").isJsonArray()) { + JsonArray arr = obj.getAsJsonArray("authors"); + for (JsonElement e : arr) { + container.authors.add(e.getAsString()); + } + } + + container.description = new ArrayList<>(); + if (obj.has("description") && obj.get("description").isJsonArray()) { + JsonArray arr = obj.getAsJsonArray("description"); + for (JsonElement e : arr) { + container.description.add(e.getAsString()); + } + } + + container.dependencies = new HashMap<>(); + container.dependencies.put(ModCore.MODID, VersionParser.parseVersionReference(ModCore.MODID + "@" + "[1.2,1.3)")); + if (obj.has("dependencies") && obj.get("dependencies").isJsonObject()) { + JsonObject depsObj = obj.getAsJsonObject("dependencies"); + String[] keys = new String[]{"default", ModCore.semanticVersion()}; + for (String key : keys) { + if (depsObj.has(key)) { + JsonArray value = depsObj.getAsJsonArray(key); + for (JsonElement depElem : value.getAsJsonArray()) { + JsonObject depObj = depElem.getAsJsonObject(); + ArtifactVersion artifact = VersionParser.parseVersionReference( + String.format("%s@%s", + getString(depObj, "modId").orElseThrow(form(candidate, "dependencies.modId")), + getString(depObj, "versionRange").orElse("[,]"))); + container.dependencies.put(artifact.getLabel(), artifact); + } + } + } + } + + container.bindMetadata(null); + return container; + } catch (IOException e) { + throw new RuntimeException("Failed to parse UMC mod", e); + } + } + + private static Optional getString(JsonObject obj, String member) { + if (obj.has(member) && obj.get(member).isJsonPrimitive()) { + return Optional.of(obj.get(member).getAsString()); + } + return Optional.empty(); + } + + private static Supplier form(ModCandidate source, String field) { + return () -> new NoSuchElementException(String.format("Failed to get mandatory field '%s' in UMC mod %s", field, source.getModContainer().getName())); + } +} \ No newline at end of file diff --git a/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinJarDiscoverer.java b/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinJarDiscoverer.java new file mode 100644 index 000000000..06e5aacfd --- /dev/null +++ b/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinJarDiscoverer.java @@ -0,0 +1,40 @@ +package cam72cam.mod.mixin.feat.mod_loading; + +import cam72cam.mod.loader.UMCModContainer; +import cam72cam.mod.loader.UMCModParser; +import net.minecraftforge.fml.common.FMLLog; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.common.discovery.ASMDataTable; +import net.minecraftforge.fml.common.discovery.JarDiscoverer; +import net.minecraftforge.fml.common.discovery.ModCandidate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +@Mixin(JarDiscoverer.class) +public class MixinJarDiscoverer { + @Inject(method = "discover", at = @At("HEAD"), remap = false, cancellable = true) + public void discover(ModCandidate candidate, ASMDataTable table, CallbackInfoReturnable> cir) { + try (JarFile file = new JarFile(candidate.getModContainer())) { + ZipEntry entry = file.getEntry("umc.json"); + if (entry != null) { + try (InputStream stream = file.getInputStream(entry)) { + UMCModContainer container = UMCModParser.parse(candidate, stream); + cir.setReturnValue(Collections.singletonList(container)); + } catch (Exception e) { + throw new RuntimeException("Failed to parse UMC mod", e); + } + } + } catch (Exception e) { + FMLLog.log.warn("Zip file {} failed to read properly, it will be ignored", candidate.getModContainer().getName(), e); + cir.setReturnValue(Collections.emptyList()); + } + } +} diff --git a/src/main/resources/mixins.feat.universalmodcore.json b/src/main/resources/mixins.feat.universalmodcore.json index 4f73bae51..c32affd14 100644 --- a/src/main/resources/mixins.feat.universalmodcore.json +++ b/src/main/resources/mixins.feat.universalmodcore.json @@ -11,7 +11,8 @@ "minVersion": "0.5.0" }, "mixins": [ - "large_entity_collision.MixinVanillaWorld" + "large_entity_collision.MixinVanillaWorld", + "mod_loading.MixinJarDiscoverer" ], "client": [ "global_renderer.MixinRenderGlobal" From 90193904457d51042a036d42c4d8f77160f21037 Mon Sep 17 00:00:00 2001 From: Goldenfield192 <1437356849@qq.com> Date: Wed, 10 Jun 2026 12:53:35 +0800 Subject: [PATCH 2/2] feat: add development mod loader --- .../cam72cam/mod/loader/UMCModContainer.java | 72 ++++++++++++++++++- .../cam72cam/mod/loader/UMCModParser.java | 6 +- .../mod_loading/MixinDirectoryDiscoverer.java | 47 ++++++++++++ .../mixins.feat.universalmodcore.json | 1 + 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinDirectoryDiscoverer.java diff --git a/src/main/java/cam72cam/mod/loader/UMCModContainer.java b/src/main/java/cam72cam/mod/loader/UMCModContainer.java index f243166b7..003c260d3 100644 --- a/src/main/java/cam72cam/mod/loader/UMCModContainer.java +++ b/src/main/java/cam72cam/mod/loader/UMCModContainer.java @@ -3,7 +3,9 @@ import cam72cam.mod.ModCore; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; +import net.minecraft.client.resources.FolderResourcePack; import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.client.FMLFolderResourcePack; import net.minecraftforge.fml.common.*; import net.minecraftforge.fml.common.discovery.ModCandidate; import net.minecraftforge.fml.common.event.FMLConstructionEvent; @@ -12,9 +14,15 @@ import net.minecraftforge.fml.common.versioning.VersionRange; import org.apache.logging.log4j.message.FormattedMessage; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.*; public class UMCModContainer extends DummyModContainer { @@ -31,6 +39,7 @@ public class UMCModContainer extends DummyModContainer { URL modURL; String license; Map dependencies; + File resourcesRoot; ModMetadata meta; @@ -74,7 +83,7 @@ public void bindMetadata(MetadataCollection mc) { meta.version = version; meta.autogenerated = false; meta.authorList = authors; - meta.url = modURL == null ? null : modURL.toString(); + meta.url = modURL == null ? "" : modURL.toString(); meta.description = String.join("\n", description); meta.dependencies = new ArrayList<>(dependencies.values()); } @@ -129,6 +138,12 @@ public void constructMod(FMLConstructionEvent event) { } } + @Override + public Class getCustomResourcePackClass() + { + return resourcesRoot != null ? DevFolderPack.class : null; + } + @Override public Object getMod() { return instance; @@ -168,4 +183,59 @@ public int getClassVersion() { public String toString() { return String.format("UMC Mod %s:%s", getName(), getVersion()); } + + public static class DevFolderPack extends FolderResourcePack implements FMLContainerHolder { + private final ModContainer container; + + public DevFolderPack(ModContainer container) + { + super(container instanceof UMCModContainer ? ((UMCModContainer) container).resourcesRoot : container.getSource()); + this.container = container; + } + + @Override + protected boolean hasResourceName(String name) + { + return super.hasResourceName(name); + } + @Override + public String getPackName() + { + return "DevFolderPack:"+container.getName(); + } + @Override + protected InputStream getInputStreamByName(String resourceName) throws IOException + { + try + { + return super.getInputStreamByName(resourceName); + } + catch (IOException exception) + { + if ("pack.mcmeta".equals(resourceName)) { + //Generate a dummy pack.mcmeta + return new ByteArrayInputStream(("{\n" + + " \"pack\": {\n"+ + " \"description\": \"UMC generated pack for "+container.getName()+"\",\n"+ + " \"pack_format\": 2\n"+ + "}\n" + + "}").getBytes(StandardCharsets.UTF_8)); + } else { + throw exception; + } + } + } + + @Override + public BufferedImage getPackImage() throws IOException + { + return ImageIO.read(getInputStreamByName(container.getMetadata().logoFile)); + } + + @Override + public ModContainer getFMLContainer() + { + return container; + } + } } diff --git a/src/main/java/cam72cam/mod/loader/UMCModParser.java b/src/main/java/cam72cam/mod/loader/UMCModParser.java index fed53fa16..3758c17c5 100644 --- a/src/main/java/cam72cam/mod/loader/UMCModParser.java +++ b/src/main/java/cam72cam/mod/loader/UMCModParser.java @@ -14,8 +14,11 @@ public class UMCModParser { private static final JsonParser parser = new JsonParser(); - //TODO not only jar but also directory public static UMCModContainer parse(ModCandidate candidate, InputStream stream) throws NoSuchElementException { + return parse(candidate, stream, null); + } + + public static UMCModContainer parse(ModCandidate candidate, InputStream stream, File resources) throws NoSuchElementException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { JsonElement root = parser.parse(reader); JsonObject obj = root.getAsJsonObject(); @@ -33,6 +36,7 @@ public static UMCModContainer parse(ModCandidate candidate, InputStream stream) container.modURL = new URL(modURL); } container.license = getString(obj, "license").orElse("N/A"); + container.resourcesRoot = resources; container.authors = new ArrayList<>(); if (obj.has("authors") && obj.get("authors").isJsonArray()) { diff --git a/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinDirectoryDiscoverer.java b/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinDirectoryDiscoverer.java new file mode 100644 index 000000000..9d03f61c7 --- /dev/null +++ b/src/main/java/cam72cam/mod/mixin/feat/mod_loading/MixinDirectoryDiscoverer.java @@ -0,0 +1,47 @@ +package cam72cam.mod.mixin.feat.mod_loading; + +import cam72cam.mod.ModCore; +import cam72cam.mod.loader.UMCModParser; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.common.discovery.ASMDataTable; +import net.minecraftforge.fml.common.discovery.DirectoryDiscoverer; +import net.minecraftforge.fml.common.discovery.ModCandidate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +@Mixin(DirectoryDiscoverer.class) +public class MixinDirectoryDiscoverer { + @Inject(method = "discover", at = @At("HEAD"), remap = false, cancellable = true) + public void discover(ModCandidate candidate, ASMDataTable table, CallbackInfoReturnable> cir) { + File classes = candidate.getModContainer(); + //Only handle this in dev environment, players should always use jar + if (classes.getAbsolutePath().contains("build" + File.separator + "classes") && ModCore.isDevelopmentEnvironment()) { + //Tricky handling! + //I love LaunchWrapper + File resources; + try { + //Cleanroom + resources = (File) ModCandidate.class.getMethod("getResourcePathRoot").invoke(candidate); + } catch (Exception e) { + //Forge + resources = new File(classes.getAbsolutePath().replace("classes"+File.separator+"java", "resources")); + } + File umcMod = new File(resources, "umc.json"); + if (resources.isDirectory() && umcMod.exists()) { + try (InputStream stream = Files.newInputStream(umcMod.toPath())) { + cir.setReturnValue(Collections.singletonList(UMCModParser.parse(candidate, stream, resources))); + } catch (Exception e) { + throw new RuntimeException("Failed to parse UMC mod", e); + } + } + } + } +} diff --git a/src/main/resources/mixins.feat.universalmodcore.json b/src/main/resources/mixins.feat.universalmodcore.json index c32affd14..f024108d5 100644 --- a/src/main/resources/mixins.feat.universalmodcore.json +++ b/src/main/resources/mixins.feat.universalmodcore.json @@ -12,6 +12,7 @@ }, "mixins": [ "large_entity_collision.MixinVanillaWorld", + "mod_loading.MixinDirectoryDiscoverer", "mod_loading.MixinJarDiscoverer" ], "client": [