diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java index 26bb7ceb2..845c81105 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java @@ -214,4 +214,5 @@ public final class LibraryAnalyzer implements Iterable { private final GameRepository repository; @@ -68,7 +65,10 @@ public class MaintainTask extends Task { return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false); } else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) { // Forge 1.13 and OptiFine - return maintainOptiFineLibrary(repository, maintainGameWithModLauncher(repository, unique(version)), true); + return maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true); + } else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) { + // Forge 1.17 + return maintainGameWithCpwBoostrapLauncher(repository, unique(version)); } else { // Vanilla Minecraft does not need maintain // Fabric does not need maintain, nothing compatible with fabric now. @@ -122,7 +122,7 @@ public class MaintainTask extends Task { return mainClass == null ? ret : ret.setMainClass(mainClass); } - private static Version maintainGameWithModLauncher(GameRepository repository, Version version) { + private static Version maintainGameWithCpwModLauncher(GameRepository repository, Version version) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); VersionLibraryBuilder builder = new VersionLibraryBuilder(version); @@ -149,6 +149,59 @@ public class MaintainTask extends Task { return builder.build(); } + private static String updateIgnoreList(GameRepository repository, Version version, String ignoreList) { + String[] ignores = ignoreList.split(","); + List newIgnoreList = new ArrayList<>(); + + // To resolve the the problem that name of primary jar may conflict with the module naming convention, + // we need to manually ignore ${primary_jar}. + newIgnoreList.add("${primary_jar}"); + + Path libraryDirectory = repository.getLibrariesDirectory(version).toPath().toAbsolutePath(); + + // The default ignoreList is too loose and may cause some problems, we replace them with the absolute version. + // For example, if "client-extra" is in ignoreList, and game directory contains "client-extra" component, all + // libraries will be ignored, which is not expected. + for (String classpathName : repository.getClasspath(version)) { + Path classpathFile = Paths.get(classpathName).toAbsolutePath(); + String fileName = classpathFile.getFileName().toString(); + if (Stream.of(ignores).anyMatch(fileName::contains)) { + // This library should be ignored for Jigsaw module finding by Forge. + String absolutePath; + if (classpathFile.startsWith(libraryDirectory)) { + // Note: It's assumed using "/" instead of File.separator in classpath + absolutePath = "${library_directory}${file_separator}" + libraryDirectory.relativize(classpathFile).toString().replace(File.separator, "${file_separator}"); + } else { + absolutePath = classpathFile.toString(); + } + newIgnoreList.add(StringUtils.substringBefore(absolutePath, ",")); + } + } + return String.join(",", newIgnoreList); + } + + // Fix wrong configurations when launching 1.17+ with Forge. + private static Version maintainGameWithCpwBoostrapLauncher(GameRepository repository, Version version) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); + VersionLibraryBuilder builder = new VersionLibraryBuilder(version); + + if (!libraryAnalyzer.has(FORGE)) return version; + + // The default ignoreList set by Forge installer does not fulfill our requirements + List jvm = builder.getMutableJvmArguments(); + for (int i = 0; i < jvm.size(); i++) { + Argument jvmArg = jvm.get(i); + if (jvmArg instanceof StringArgument) { + String jvmArgStr = jvmArg.toString(); + if (jvmArgStr.startsWith("-DignoreList=")) { + jvm.set(i, new StringArgument("-DignoreList=" + updateIgnoreList(repository, version, jvmArgStr.substring("-DignoreList=".length())))); + } + } + } + + return builder.build(); + } + private static Version maintainOptiFineLibrary(GameRepository repository, Version version, boolean remove) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); List libraries = new ArrayList<>(version.getLibraries()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index dcb18ec26..01c4ca9f6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -173,7 +173,7 @@ public final class ForgeInstallTask extends Task { ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); if (!gameVersion.get().equals(profile.getMinecraft())) throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); - return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getPath().getVersion().replaceAll("(?i)forge", "")), installer); + return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getVersion()), installer); } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class); if (!gameVersion.get().equals(profile.getInstall().getMinecraft())) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java index 8b6464ddf..d388403cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallProfile.java @@ -25,10 +25,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalFunction; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Immutable @@ -37,15 +34,17 @@ public class ForgeNewInstallProfile implements Validation { private final int spec; private final String minecraft; private final String json; + private final String version; private final Artifact path; private final List libraries; private final List processors; private final Map data; - public ForgeNewInstallProfile(int spec, String minecraft, String json, Artifact path, List libraries, List processors, Map data) { + public ForgeNewInstallProfile(int spec, String minecraft, String json, String version, Artifact path, List libraries, List processors, Map data) { this.spec = spec; this.minecraft = minecraft; this.json = json; + this.version = version; this.path = path; this.libraries = libraries; this.processors = processors; @@ -74,12 +73,20 @@ public class ForgeNewInstallProfile implements Validation { return json; } + /** + * + * @return forge version. + */ + public String getVersion() { + return version; + } + /** * Maven artifact path for the main jar to install. * @return artifact path of the main jar. */ - public Artifact getPath() { - return path; + public Optional getPath() { + return Optional.ofNullable(path); } /** @@ -112,7 +119,7 @@ public class ForgeNewInstallProfile implements Validation { @Override public void validate() throws JsonParseException, TolerableValidationException { - if (minecraft == null || json == null || path == null) + if (minecraft == null || json == null || version == null) throw new JsonParseException("ForgeNewInstallProfile is malformed"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index 5d2bd78ab..edd7eac35 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -76,6 +76,50 @@ public class ForgeNewInstallTask extends Task { setSignificance(TaskSignificance.MINOR); } + private static String replaceTokens(Map tokens, String value) { + StringBuilder buf = new StringBuilder(); + for (int x = 0; x < value.length(); x++) { + char c = value.charAt(x); + if (c == '\\') { + if (x == value.length() - 1) + throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value); + buf.append(value.charAt(++x)); + } else if (c == '{' || c == '\'') { + StringBuilder key = new StringBuilder(); + for (int y = x + 1; y <= value.length(); y++) { + if (y == value.length()) + throw new IllegalArgumentException("Illegal pattern (Unclosed " + c + "): " + value); + char d = value.charAt(y); + if (d == '\\') { + if (y == value.length() - 1) + throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value); + key.append(value.charAt(++y)); + } else { + if (c == '{' && d == '}') { + x = y; + break; + } + if (c == '\'' && d == '\'') { + x = y; + break; + } + key.append(d); + } + } + if (c == '\'') { + buf.append(key); + } else { + if (!tokens.containsKey(key.toString())) + throw new IllegalArgumentException("Illegal pattern: " + value + " Missing Key: " + key); + buf.append(tokens.get(key.toString())); + } + } else { + buf.append(c); + } + } + return buf.toString(); + } + private String parseLiteral(String literal, Map var, ExceptionalFunction plainConverter) throws E { if (StringUtils.isSurrounded(literal, "{", "}")) return var.get(StringUtils.removeSurrounding(literal, "{", "}")); @@ -84,7 +128,7 @@ public class ForgeNewInstallTask extends Task { else if (StringUtils.isSurrounded(literal, "[", "]")) return gameRepository.getArtifactFile(version, Artifact.fromDescriptor(StringUtils.removeSurrounding(literal, "[", "]"))).toString(); else - return plainConverter.apply(literal); + return plainConverter.apply(replaceTokens(var, literal)); } @Override @@ -116,10 +160,10 @@ public class ForgeNewInstallTask extends Task { } } - { - Path mainJar = profile.getPath().getPath(fs.getPath("maven")); + if (profile.getPath().isPresent()) { + Path mainJar = profile.getPath().get().getPath(fs.getPath("maven")); if (Files.exists(mainJar)) { - Path dest = gameRepository.getArtifactFile(version, profile.getPath()); + Path dest = gameRepository.getArtifactFile(version, profile.getPath().get()); FileUtils.copyFile(mainJar, dest); } } @@ -153,6 +197,10 @@ public class ForgeNewInstallTask extends Task { data.put("SIDE", "client"); data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath()); + data.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath()); + data.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath()); + data.put("INSTALLER", installer.toAbsolutePath().toString()); + data.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath()); for (ForgeNewInstallProfile.Processor processor : processors) { Map outputs = new HashMap<>(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java index 1913414ed..6eda47f52 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -21,7 +21,9 @@ import org.jackhuang.hmcl.task.Task; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; /** * Supports operations on versioning. @@ -220,4 +222,14 @@ public interface GameRepository extends VersionProvider { */ File getLoggingObject(String version, String assetId, LoggingInfo loggingInfo); + default List getClasspath(Version version) { + List classpath = new ArrayList<>(); + for (Library library : version.getLibraries()) + if (library.appliesToCurrentEnvironment() && !library.isNative()) { + File f = getLibraryFile(version, library); + if (f.exists() && f.isFile()) + classpath.add(f.getAbsolutePath()); + } + return classpath; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java index 272c0aebb..481f76abe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java @@ -20,7 +20,8 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * @@ -145,6 +146,11 @@ public final class VersionLibraryBuilder { } } + public List getMutableJvmArguments() { + jvmChanged = true; + return jvm; + } + public void addGameArgument(String... args) { for (String arg : args) game.add(new StringArgument(arg)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 291796068..81828a8fc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -160,13 +160,7 @@ public class DefaultLauncher extends Launcher { } } - LinkedList classpath = new LinkedList<>(); - for (Library library : version.getLibraries()) - if (library.appliesToCurrentEnvironment() && !library.isNative()) { - File f = repository.getLibraryFile(version, library); - if (f.exists() && f.isFile()) - classpath.add(f.getAbsolutePath()); - } + List classpath = repository.getClasspath(version); File jar = repository.getVersionJar(version); if (!jar.exists() || !jar.isFile()) @@ -308,7 +302,9 @@ public class DefaultLauncher extends Launcher { // libraries_directory stands for historical reasons here. We don't know the official launcher // had already defined "library_directory" as the placeholder for path to ".minecraft/libraries" // when we propose this placeholder. - pair("${libraries_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()) + pair("${libraries_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()), + // file_separator is used in -DignoreList + pair("${file_separator}", OperatingSystem.FILE_SEPARATOR) ); }