Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loading via 3rd party mod on 1.17+ forge #19

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
97a73ac
stage0/ml9: create mixin bootstrap
Sychic Jun 18, 2024
aef0ceb
stage0/ml9: refactor transformation service file to container only
Sychic Jun 18, 2024
6410897
stage1/ml9: create mixin bootstrap
Sychic Jun 18, 2024
eac3a3f
stage2/ml9: create mixin bootstrap
Sychic Jun 18, 2024
329bbd8
stage1/ml9: make transformation service path compatible with mixin bo…
Sychic Jun 18, 2024
1e7be5b
stage2/ml9: adjust restart message to be more clear
Sychic Jun 19, 2024
a572fdf
stage2/ml9: change to downloading to a temp file and moving it on com…
Sychic Jun 19, 2024
72015f4
Revert "stage0/ml9: refactor transformation service file to container…
Sychic Jun 19, 2024
472f1c9
stage0/ml9: move load to constructor instead of `onLoad`
Sychic Jun 19, 2024
54c2da3
stage1/ml9: switch back to looking for `ITransformationService`
Sychic Jun 19, 2024
7c6a482
stage1/ml9: use same key for mixin path loaded
Sychic Jun 19, 2024
8afb6f3
stage1/ml9: use same stage2 path as transformation service
Sychic Jun 19, 2024
cc77ad2
stage2/ml9: handle dedicated jar loader exceptions
Sychic Jun 19, 2024
966eb04
stage2/ml9: use forked jvm for restart ui
Sychic Jun 19, 2024
567b52c
stage2/ml9: flip game and mod version for dedicated jar
Sychic Jun 19, 2024
a1a5624
stage2/ml9: acquire version first and use it for download meta
Sychic Jun 19, 2024
3df7c68
stage2/ml9: use dashed mc version for api
Sychic Jun 19, 2024
552c6cb
stage2/ml9: adjust restart ui wording
Sychic Jun 19, 2024
68b1918
stage2/ml9: change exception handling to stop showing restart ui when…
Sychic Jun 20, 2024
f7e7347
stage2/ml9: reword restart ui using mod name marker file
Sychic Jun 27, 2024
127a548
stage2/ml9: default empty mod names list to "Unknown"
Sychic Jun 28, 2024
0995ac5
stage0/ml9: remove unnecessary mixin config
Sychic Aug 24, 2024
28a3967
stage1/ml9: use same variable for mixin path mc version
Sychic Aug 24, 2024
61f3611
stage2/ml9: add explicit line break to `RestartUI`
Sychic Aug 24, 2024
35136cb
stage2/ml9: fix logic error in `ForkedRestartUI`
Sychic Aug 24, 2024
a285f5f
stage2/ml9: remove boolean return type from `RestartUI#waitForClose`
Sychic Aug 24, 2024
66e4e40
stage2/ml9: make sure temp file is deleted
Sychic Aug 24, 2024
ed78429
stage2/ml9: move where file is moved and change temp file name
Sychic Aug 24, 2024
2955dfd
stage1/ml9: improve comment in `EssentialMixinPluginLoader`
Sychic Aug 24, 2024
80bbf26
stage2/ml9: fix using wrong method to wait for forked process to term…
Sychic Aug 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion stage0/modlauncher9/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ dependencies {
compileOnly("cpw.mods:securejarhandler:0.9.50")
compileOnly("net.sf.jopt-simple:jopt-simple:5.0.4")
compileOnly("org.jetbrains:annotations:21.0.1")
}
// as of forge 37.1 for MC 1.17.1, mixin is shipped with forge
// https://github.com/MinecraftForge/MinecraftForge/commit/c8d3ad1d1c977513b75e6aa3dd81c6ed9463bcae
compileOnly("org.spongepowered:mixin:0.8.5")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package gg.essential.loader.stage0;

import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.api.IEnvironment;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;

import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;

import static gg.essential.loader.stage0.EssentialLoader.STAGE1_PKG;

@SuppressWarnings("unused")
public class EssentialStage0MixinPlugin implements IMixinConfigPlugin {
private static final String STAGE1_CLS = STAGE1_PKG + "EssentialMixinPluginLoader";

public EssentialStage0MixinPlugin() throws Exception {
loadStage1(this);
}

@Override
public void onLoad(String mixinPackage) {
}

private static void loadStage1(Object stage0) throws Exception {
Path gameDir = Launcher.INSTANCE.environment()
.getProperty(IEnvironment.Keys.GAMEDIR.get())
.orElseGet(() -> Paths.get("."));

final EssentialLoader loader = new EssentialLoader("modlauncher"); // using same variant as the transformation service
final Path stage1File = loader.loadStage1File(gameDir);
final URL stage1Url = stage1File.toUri().toURL();

// Create a class loader with which to load stage1
URLClassLoader classLoader = new URLClassLoader(new URL[]{ stage1Url }, stage0.getClass().getClassLoader());

Class.forName(STAGE1_CLS, true, classLoader)
.getConstructor()
.newInstance();
}

@Override
public String getRefMapperConfig() {
return null;
}

@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return false;
}

@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {

}

@Override
public List<String> getMixins() {
return null;
}

@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {

}

@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.util.stream.Collectors;

public abstract class EssentialTransformationServiceBase extends DelegatingTransformationServiceBase {
private static final String KEY_LOADED = "gg.essential.loader.stage1.loaded";
protected static final String KEY_LOADED = "gg.essential.loader.stage1.loaded";

public EssentialTransformationServiceBase(
final ITransformationService stage0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gg.essential.loader.stage1;

import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.api.IEnvironment;
import cpw.mods.modlauncher.api.ITransformationService;
import cpw.mods.modlauncher.api.TypesafeMap;

import java.nio.file.Path;
import java.nio.file.Paths;

import static gg.essential.loader.stage1.EssentialTransformationService.MC_VERSION;
import static gg.essential.loader.stage1.EssentialTransformationServiceBase.KEY_LOADED;

@SuppressWarnings("unused")
public class EssentialMixinPluginLoader {

public EssentialMixinPluginLoader() throws Exception {
// Check if stage 2 has already been loaded by a transformation service
final TypesafeMap blackboard = Launcher.INSTANCE.blackboard();
final TypesafeMap.Key<ITransformationService> LOADED =
TypesafeMap.Key.getOrCreate(blackboard, KEY_LOADED, ITransformationService.class);
if (blackboard.get(LOADED).isPresent()) {
return;
}

final Path gameDir = Launcher.INSTANCE.environment()
.getProperty(IEnvironment.Keys.GAMEDIR.get())
.orElse(Paths.get("."));

// variant and game version to match transformation service path
EssentialLoader loader = EssentialLoader.getInstance("modlauncher9", "forge_" + MC_VERSION);
loader.load(gameDir);

loader.getStage2().getClass()
.getMethod("loadFromMixin", Path.class)
.invoke(loader.getStage2(), gameDir);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class EssentialTransformationService extends EssentialTransformationServi
// the first one we support (stage2 will be the same for all MC versions on the same stage1 anyway) and once we are
// in stage2, we can re-query at a later point as needed (but we always retain the ability to run an up-to-date
// stage2 as early as possible).
private static final String MC_VERSION = "1.17.1";
protected static final String MC_VERSION = "1.17.1";

public EssentialTransformationService(final ITransformationService stage0) throws Exception {
super(stage0, FallbackTransformationService::new, "modlauncher9", MC_VERSION);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package gg.essential.loader.stage2;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;

import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;

public class DedicatedJarLoader {
private static final Logger LOGGER = LogManager.getLogger(DedicatedJarLoader.class);
private static final String BASE_URL = System.getProperty(
"essential.download.url",
System.getenv().getOrDefault("ESSENTIAL_DOWNLOAD_URL", "https://api.essential.gg/mods")
);
private static final String VERSION_URL = BASE_URL + "/v1/essential:essential-pinned/versions/%s/platforms/%s";
private static final String DOWNLOAD_URL = VERSION_URL + "/download";

protected static void downloadDedicatedJar(LoaderUI ui, Path modsDir, String gameVersion) throws IOException {
final String apiVersion = gameVersion.replace('.', '-');
final String essentialVersion = getEssentialVersionMeta(apiVersion).get("version").getAsString();

final JsonObject meta = getEssentialDownloadMeta(essentialVersion, apiVersion);
final URL url = new URL(meta.get("url").getAsString());
final URLConnection connection = url.openConnection();

ui.setDownloadSize(connection.getContentLength());

final Path target = modsDir.resolve(String.format("Essential %s (%s).jar", essentialVersion, gameVersion));
final Path tempFile = Files.createFile(modsDir.resolve("Essential Jar Download.jar.tmp"));

try (
final InputStream in = connection.getInputStream();
final OutputStream out = Files.newOutputStream(tempFile);
) {
final byte[] buffer = new byte[1024];
int totalRead = 0;
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
totalRead += read;
ui.setDownloaded(totalRead);
}
} finally {
try {
Files.move(tempFile, target, ATOMIC_MOVE);
} finally {
Files.deleteIfExists(tempFile);
}
}
}

private static JsonObject getEssentialMeta(URL url) throws IOException {
String response;
try (final InputStream in = url.openStream()) {
response = new String(in.readAllBytes());
}
JsonElement json = new JsonParser().parse(response);
return json.getAsJsonObject();
}

private static JsonObject getEssentialVersionMeta(String gameVersion) throws IOException {
return getEssentialMeta(new URL(String.format(VERSION_URL, "stable", gameVersion)));
}

private static JsonObject getEssentialDownloadMeta(String essentialVersion, String gameVersion) throws IOException {
return getEssentialMeta(new URL(String.format(DOWNLOAD_URL, essentialVersion, gameVersion)));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
package gg.essential.loader.stage2;

import cpw.mods.modlauncher.api.ITransformationService;
import gg.essential.loader.stage2.components.ForkedRestartUI;
import gg.essential.loader.stage2.components.RestartUI;
import gg.essential.loader.stage2.jvm.ForkedJvmLoaderSwingUI;
import net.minecraftforge.fml.loading.FMLLoader;
import org.apache.logging.log4j.LogManager;

import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* The initial entrypoint for stage2. With ModLauncher, we cannot yet know the MC version at this point, so this is a
Expand All @@ -26,4 +38,32 @@ public ITransformationService getTransformationService() {
public void load() {
// delayed until ModLauncher exposes the MC version
}

@SuppressWarnings("unused") // called via reflection from stage1
public void loadFromMixin(Path gameDir) throws Exception {
final Path modsDir = gameDir.resolve("mods");
LoaderUI ui = LoaderUI.all(
new LoaderLoggingUI().updatesEveryMillis(1000),
new ForkedJvmLoaderSwingUI().updatesEveryMillis(1000 / 60)
);
ui.start();
try {
DedicatedJarLoader.downloadDedicatedJar(ui, modsDir, "forge_" + FMLLoader.versionInfo().mcVersion());
} finally {
ui.complete();
}
List<URL> modNameMarkers = Collections.list(this.getClass().getClassLoader().getResources("META-INF/essential-loader-mod-name.txt"));
List<String> modNames = new ArrayList<>();
for (URL url : modNameMarkers) {
String modName = Files.readString(Paths.get(url.toURI()));
modNames.add(modName);
}
if (modNames.isEmpty()) {
modNames = List.of("Unknown");
}
ForkedRestartUI restartUI = new ForkedRestartUI(modNames);
restartUI.show();
restartUI.waitForClose();
System.exit(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package gg.essential.loader.stage2.components;

import gg.essential.loader.stage2.jvm.ForkedJvm;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ForkedRestartUI {
private final Logger LOGGER = LogManager.getLogger();
private final List<String> mods;
private ForkedJvm jvm;

public ForkedRestartUI(List<String> mods) {
this.mods = mods;
}

public void show() {
try {
this.jvm = new ForkedJvm(getClass());

DataOutputStream out = new DataOutputStream(this.jvm.process.getOutputStream());
for (String name : this.mods) {
out.writeBoolean(true); // signal more entries
out.writeUTF(name);
}
out.writeBoolean(false); // signal end of list
out.flush();
} catch (IOException e) {
LOGGER.warn("Failed to fork JVM for RestartUI:", e);
}
}

public void waitForClose() {
if (this.jvm == null) return;

try {
this.jvm.process.getInputStream().read();
} catch (IOException e) {
LOGGER.warn("Failed to wait for RestartUI to close:", e);
} finally {
this.jvm.close();
this.jvm = null;
}
}

public static void main(String[] args) throws IOException {
DataInputStream in = new DataInputStream(System.in);

List<String> mods = new ArrayList<>();
while(in.readBoolean()) {
mods.add(in.readUTF());
}

try {
RestartUI ui = new RestartUI(mods);
ui.show();

ui.waitForClose();
} catch (Throwable t) {
t.printStackTrace();
}

System.out.flush();
System.out.close();
}
}
Loading
Loading