重构 MultiMC 整合包兼容功能 (#3547)

Fix #3540
Fix #3706
This commit is contained in:
Burning_TNT 2025-06-08 20:59:44 +08:00 committed by GitHub
parent 88a7243b32
commit 7d7dbb9eb5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 758 additions and 205 deletions

View File

@ -281,7 +281,13 @@ public class InstallerItem extends Control {
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
item.resolvedStateProperty.bind(item.versionProperty);
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
InstalledState itemVersion = item.versionProperty.get();
if (itemVersion != null) {
return itemVersion;
}
return InstallableState.INSTANCE;
}, item.versionProperty));
}
}
@ -385,7 +391,10 @@ public class InstallerItem extends Control {
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
State state = control.resolvedStateProperty.get();
return state instanceof InstalledState && !((InstalledState) state).external;
}, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.setOnAction(e -> {

View File

@ -124,7 +124,7 @@ public final class FabricInstallTask extends Task<Version> {
libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.intermediary.maven), "https://maven.fabricmc.net/", null));
libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.loader.maven), "https://maven.fabricmc.net/", null));
return new Version(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, 30000, arguments, mainClass, libraries);
return new Version(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries);
}
public static class FabricInfo {

View File

@ -410,7 +410,7 @@ public class ForgeNewInstallTask extends Task<Version> {
dependencyManager.checkLibraryCompletionAsync(forgeVersion, true)));
setResult(forgeVersion
.setPriority(30000)
.setPriority(Version.PRIORITY_LOADER)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
}

View File

@ -82,7 +82,7 @@ public class ForgeOldInstallTask extends Task<Version> {
}
setResult(installProfile.getVersionInfo()
.setPriority(30000)
.setPriority(Version.PRIORITY_LOADER)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo(), true));

View File

@ -65,7 +65,7 @@ public class GameInstallTask extends Task<Version> {
@Override
public void execute() throws Exception {
Version patch = JsonUtils.fromNonNullJson(downloadTask.getResult(), Version.class)
.setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(0);
.setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(Version.PRIORITY_MC);
setResult(patch);
Version version = new Version(this.version.getId()).addPatch(patch);

View File

@ -406,7 +406,7 @@ public class NeoForgeOldInstallTask extends Task<Version> {
dependencyManager.checkLibraryCompletionAsync(neoForgeVersion, true)));
setResult(neoForgeVersion
.setPriority(30000)
.setPriority(Version.PRIORITY_LOADER)
.setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId())
.setVersion(selfVersion));
}

View File

@ -120,7 +120,7 @@ public final class QuiltInstallTask extends Task<Version> {
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null));
libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.loader.maven), getMavenRepositoryByGroup(quiltInfo.loader.maven), null));
return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, 30000, arguments, mainClass, libraries);
return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries);
}
private static String getMavenRepositoryByGroup(String maven) {

View File

@ -79,8 +79,12 @@ public final class Arguments {
}
public Arguments addJVMArguments(List<String> jvmArguments) {
List<Argument> list = jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList());
return new Arguments(getGame(), Lang.merge(getJvm(), list));
return addJVMArgumentsDirect(jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList()));
}
// TODO: How to distinguish addJVMArgumentsDirect from addJVMArguments? Naming is hard :)
public Arguments addJVMArgumentsDirect(List<Argument> jvmArguments) {
return new Arguments(getGame(), Lang.merge(getJvm(), jvmArguments));
}
public static Arguments merge(Arguments a, Arguments b) {

View File

@ -95,10 +95,15 @@ public class DefaultGameRepository implements GameRepository {
@Override
public File getLibraryFile(Version version, Library lib) {
if ("local".equals(lib.getHint()) && lib.getFileName() != null)
return new File(getVersionRoot(version.getId()), "libraries/" + lib.getFileName());
else
return new File(getLibrariesDirectory(version), lib.getPath());
if ("local".equals(lib.getHint())) {
if (lib.getFileName() != null) {
return new File(getVersionRoot(version.getId()), "libraries/" + lib.getFileName());
}
return new File(getVersionRoot(version.getId()), "libraries/" + lib.getArtifact().getFileName());
}
return new File(getLibrariesDirectory(version), lib.getPath());
}
public Path getArtifactFile(Version version, Artifact artifact) {

View File

@ -17,10 +17,11 @@
*/
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
@ -28,10 +29,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
/**
* A class that describes a Minecraft dependency.
@ -40,6 +38,40 @@ import java.util.Optional;
*/
@Immutable
public class Library implements Comparable<Library>, Validation {
/**
* <p>A possible native descriptors can be: [variant-]os[-key]</p>
*
* <p>
* Variant can be empty string, 'native', or 'natives'.
* Key can be empty string, system arch, or system arch bit count.
* </p>
*/
private static final String[] POSSIBLE_NATIVE_DESCRIPTORS;
static {
String[] keys = {
"",
Architecture.SYSTEM_ARCH.name().toLowerCase(Locale.ROOT),
Architecture.SYSTEM_ARCH.getBits().getBit()
}, variants = {"", "native", "natives"};
POSSIBLE_NATIVE_DESCRIPTORS = new String[keys.length * variants.length];
StringBuilder builder = new StringBuilder();
for (int i = 0; i < keys.length; i++) {
for (int j = 0; j < variants.length; j++) {
if (!variants[j].isEmpty()) {
builder.append(variants[j]).append('-');
}
builder.append(OperatingSystem.CURRENT_OS.getMojangName());
if (!keys[i].isEmpty()) {
builder.append('-').append(keys[i]);
}
POSSIBLE_NATIVE_DESCRIPTORS[i * variants.length + j] = builder.toString();
builder.setLength(0);
}
}
}
@SerializedName("name")
private final Artifact artifact;
@ -93,13 +125,27 @@ public class Library implements Comparable<Library>, Validation {
}
public String getClassifier() {
if (artifact.getClassifier() == null)
if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS.getMojangName()))
return natives.get(OperatingSystem.CURRENT_OS.getMojangName()).replace("${arch}", Architecture.SYSTEM_ARCH.getBits().getBit());
else
return null;
else
if (artifact.getClassifier() == null) {
if (natives != null) {
for (String nativeDescriptor : POSSIBLE_NATIVE_DESCRIPTORS) {
String nd = natives.get(nativeDescriptor);
if (nd != null) {
return nd.replace("${arch}", Architecture.SYSTEM_ARCH.getBits().getBit());
}
}
} else if (downloads != null && downloads.getClassifiers() != null) {
for (String nativeDescriptor : POSSIBLE_NATIVE_DESCRIPTORS) {
LibraryDownloadInfo info = downloads.getClassifiers().get(nativeDescriptor);
if (info != null) {
return nativeDescriptor;
}
}
}
return null;
} else {
return artifact.getClassifier();
}
}
public ExtractRules getExtract() {
@ -111,10 +157,17 @@ public class Library implements Comparable<Library>, Validation {
}
public boolean isNative() {
return natives != null && appliesToCurrentEnvironment();
if (!appliesToCurrentEnvironment()) {
return false;
}
if (natives != null) {
return true;
}
return downloads != null && downloads.getClassifiers().keySet().stream().anyMatch(s -> s.startsWith("native"));
}
protected LibraryDownloadInfo getRawDownloadInfo() {
public LibraryDownloadInfo getRawDownloadInfo() {
if (downloads != null) {
if (isNative())
return downloads.getClassifiers().get(getClassifier());
@ -125,6 +178,10 @@ public class Library implements Comparable<Library>, Validation {
}
}
public Artifact getArtifact() {
return artifact;
}
public String getPath() {
LibraryDownloadInfo temp = getRawDownloadInfo();
if (temp != null && temp.getPath() != null)
@ -137,12 +194,28 @@ public class Library implements Comparable<Library>, Validation {
LibraryDownloadInfo temp = getRawDownloadInfo();
String path = getPath();
return new LibraryDownloadInfo(path,
Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path),
computePath(temp, path),
temp != null ? temp.getSha1() : null,
temp != null ? temp.getSize() : 0
);
}
private String computePath(LibraryDownloadInfo raw, String path) {
if (raw != null) {
String url = raw.getUrl();
if (url != null) {
return url;
}
}
String repo = Lang.requireNonNullElse(url, Constants.DEFAULT_LIBRARY_URL);
if (!repo.endsWith("/")) {
repo += '/';
}
return repo + path;
}
public boolean hasDownloadURL() {
LibraryDownloadInfo temp = getRawDownloadInfo();
if (temp != null) return temp.getUrl() != null;
@ -159,6 +232,7 @@ public class Library implements Comparable<Library>, Validation {
/**
* Hint for how to locate the library file.
*
* @return null for default, "local" for location in version/&lt;version&gt;/libraries/filename
*/
@Nullable
@ -168,6 +242,7 @@ public class Library implements Comparable<Library>, Validation {
/**
* Available when hint is "local"
*
* @return the filename of the local library in version/&lt;version&gt;/libraries/$filename
*/
@Nullable

View File

@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.util.regex.Pattern;
/**
*
* @author huangyuhui
*/
public final class OSRestriction {
@ -57,19 +56,28 @@ public final class OSRestriction {
this.arch = arch;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public String getArch() {
return arch;
}
public boolean allow() {
// Some modpacks directly use { name: "win-x86" }
if (name != null) {
String[] parts = name.split("-", 3);
if (parts.length == 2) {
OperatingSystem os = OperatingSystem.parseOSName(parts[0]);
Architecture arch = Architecture.parseArchName(parts[1]);
if (os != OperatingSystem.UNKNOWN && arch != Architecture.UNKNOWN) {
if (os != OperatingSystem.CURRENT_OS && !(os == OperatingSystem.LINUX && OperatingSystem.CURRENT_OS.isLinuxOrBSD())) {
return false;
}
if (arch != Architecture.SYSTEM_ARCH) {
return false;
}
return true;
}
}
}
OperatingSystem os = OperatingSystem.parseOSName(name);
if (os != OperatingSystem.UNKNOWN
&& os != OperatingSystem.CURRENT_OS
@ -85,5 +93,4 @@ public final class OSRestriction {
return true;
}
}

View File

@ -36,6 +36,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@Immutable
public class Version implements Comparable<Version>, Validation {
/**
* Patches with higher priority can override info from other patches, such as mainClass.
*/
public static final int PRIORITY_MC = 0, PRIORITY_LOADER = 30000;
private final String id;
private final String version;
private final Integer priority;
@ -302,7 +307,8 @@ public class Version implements Comparable<Version>, Validation {
if (inheritsFrom == null) {
if (isRoot()) {
thisVersion = new Version(id).setPatches(patches);
// TODO: Breaking change, require much testing on versions installed with external installer, other launchers, and all kinds of versions.
thisVersion = patches != null ? new Version(id).setPatches(patches) : this;
} else {
thisVersion = this;
}
@ -368,7 +374,7 @@ public class Version implements Comparable<Version>, Validation {
return thisVersion.setId(id).setJar(resolve(provider).getJar());
}
private Version markAsResolved() {
public Version markAsResolved() {
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
@ -380,6 +386,10 @@ public class Version implements Comparable<Version>, Validation {
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setRoot(Boolean root) {
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setId(String id) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
@ -396,6 +406,10 @@ public class Version implements Comparable<Version>, Validation {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setJavaVersion(GameJavaVersion javaVersion) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setArguments(Arguments arguments) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
@ -412,10 +426,18 @@ public class Version implements Comparable<Version>, Validation {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setAssetIndex(AssetIndexInfo assetIndex) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setLibraries(List<Library> libraries) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setDownload(JsonMap<DownloadType, DownloadInfo> downloads) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setLogging(Map<DownloadType, LoggingInfo> logging) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}

View File

@ -18,7 +18,12 @@
package org.jackhuang.hmcl.game.tlauncher;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.ExtractRules;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.util.Immutable;
import java.util.List;

View File

@ -0,0 +1,52 @@
package org.jackhuang.hmcl.mod.multimc;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
public final class MultiMCComponents {
private MultiMCComponents() {
}
private static final Map<String, LibraryAnalyzer.LibraryType> ID_TYPE = new HashMap<>();
static {
ID_TYPE.put("net.minecraft", LibraryAnalyzer.LibraryType.MINECRAFT);
ID_TYPE.put("net.minecraftforge", LibraryAnalyzer.LibraryType.FORGE);
ID_TYPE.put("net.neoforged", LibraryAnalyzer.LibraryType.NEO_FORGE);
ID_TYPE.put("com.mumfrey.liteloader", LibraryAnalyzer.LibraryType.LITELOADER);
ID_TYPE.put("net.fabricmc.fabric-loader", LibraryAnalyzer.LibraryType.FABRIC);
ID_TYPE.put("org.quiltmc.quilt-loader", LibraryAnalyzer.LibraryType.QUILT);
}
private static final Map<LibraryAnalyzer.LibraryType, String> TYPE_ID =
ID_TYPE.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
private static final Collection<Map.Entry<String, LibraryAnalyzer.LibraryType>> PAIRS = Collections.unmodifiableCollection(ID_TYPE.entrySet());
static {
if (TYPE_ID.isEmpty()) {
throw new AssertionError("Please make sure TYPE_ID and PAIRS is initialized after ID_TYPE!");
}
}
public static String getComponent(LibraryAnalyzer.LibraryType type) {
return TYPE_ID.get(type);
}
public static LibraryAnalyzer.LibraryType getComponent(String type) {
return ID_TYPE.get(type);
}
public static Collection<Map.Entry<String, LibraryAnalyzer.LibraryType>> getPairs() {
return PAIRS;
}
public static URL getMetaURL(String componentID, String version) {
return NetworkUtils.toURL(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version));
}
}

View File

@ -17,80 +17,408 @@
*/
package org.jackhuang.hmcl.mod.multimc;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.Argument;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.AssetIndexInfo;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.DownloadType;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.OSRestriction;
import org.jackhuang.hmcl.game.RuledArgument;
import org.jackhuang.hmcl.game.StringArgument;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonMap;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
* @author huangyuhui
*/
@Immutable
public final class MultiMCInstancePatch {
private final int formatVersion;
private final String name;
@SerializedName("uid")
private final String id;
@SerializedName("version")
private final String version;
@SerializedName("mcVersion")
private final String gameVersion;
@SerializedName("assetIndex")
private final AssetIndexInfo assetIndex;
@SerializedName("minecraftArguments")
private final String minecraftArguments;
@SerializedName("+jvmArgs")
private final List<String> jvmArgs;
@SerializedName("mainClass")
private final String mainClass;
private final String fileId;
@SerializedName("compatibleJavaMajors")
private final int[] javaMajors;
@SerializedName("mainJar")
private final Library mainJar;
@SerializedName("+traits")
private final List<String> traits;
@SerializedName("+tweakers")
private final List<String> tweakers;
@SerializedName("+libraries")
private final List<Library> _libraries;
@SerializedName(value = "+libraries")
private final List<Library> libraries0;
@SerializedName(value = "libraries")
private final List<Library> libraries1;
@SerializedName(value = "mavenFiles")
private final List<Library> mavenFiles;
@SerializedName("libraries")
private final List<Library> libraries;
@SerializedName("jarMods")
private final List<Library> jarMods;
public MultiMCInstancePatch() {
this("", "", "", "", "", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
public MultiMCInstancePatch(String name, String version, String gameVersion, String mainClass, String fileId, List<String> tweakers, List<Library> _libraries, List<Library> libraries) {
this.name = name;
public MultiMCInstancePatch(int formatVersion, String id, String version, AssetIndexInfo assetIndex, String minecraftArguments, List<String> jvmArgs, String mainClass, int[] javaMajors, Library mainJar, List<String> traits, List<String> tweakers, List<Library> libraries0, List<Library> libraries1, List<Library> mavenFiles, List<Library> jarMods) {
this.formatVersion = formatVersion;
this.id = id;
this.version = version;
this.gameVersion = gameVersion;
this.assetIndex = assetIndex;
this.minecraftArguments = minecraftArguments;
this.jvmArgs = jvmArgs;
this.mainClass = mainClass;
this.fileId = fileId;
this.tweakers = new ArrayList<>(tweakers);
this._libraries = new ArrayList<>(_libraries);
this.libraries = new ArrayList<>(libraries);
this.javaMajors = javaMajors;
this.mainJar = mainJar;
this.traits = traits;
this.tweakers = tweakers;
this.libraries0 = libraries0;
this.libraries1 = libraries1;
this.mavenFiles = mavenFiles;
this.jarMods = jarMods;
}
public String getName() {
return name;
public int getFormatVersion() {
return formatVersion;
}
public String getID() {
return id;
}
public String getVersion() {
return version;
}
public String getGameVersion() {
return gameVersion;
public AssetIndexInfo getAssetIndex() {
return assetIndex;
}
public String getMinecraftArguments() {
return minecraftArguments;
}
public List<String> getJvmArgs() {
return nonNullOrEmpty(jvmArgs);
}
public String getMainClass() {
return mainClass;
}
public String getFileId() {
return fileId;
public int[] getJavaMajors() {
return javaMajors;
}
public Library getMainJar() {
return mainJar;
}
public List<String> getTraits() {
return nonNullOrEmpty(traits);
}
public List<String> getTweakers() {
return Collections.unmodifiableList(tweakers);
return nonNullOrEmpty(tweakers);
}
public List<Library> getLibraries() {
return Lang.merge(_libraries, libraries);
List<Library> list = new ArrayList<>();
if (libraries0 != null) {
list.addAll(libraries0);
}
if (libraries1 != null) {
list.addAll(libraries1);
}
return nonNullOrEmpty(list);
}
public List<Library> getMavenOnlyFiles() {
return nonNullOrEmpty(mavenFiles);
}
public List<Library> getJarMods() {
return nonNullOrEmpty(jarMods);
}
private static <T> List<T> nonNullOrEmpty(List<T> value) {
return value != null && !value.isEmpty() ? value : Collections.emptyList();
}
private static <T, K> List<T> dropDuplicate(List<T> original, Function<T, K> mapper) {
Set<K> values = new HashSet<>();
List<T> result = new ArrayList<>();
for (T item : original) {
if (values.add(mapper.apply(item))) {
result.add(item);
}
}
return result;
}
public static MultiMCInstancePatch read(String componentID, String text) {
try {
return JsonUtils.fromNonNullJson(text, MultiMCInstancePatch.class);
} catch (JsonParseException e) {
throw new IllegalArgumentException("Illegal Json-Patch: " + componentID);
}
}
public static final class ResolvedInstance {
private final Version version;
private final String gameVersion;
private final Library mainJar;
private final List<String> jarModFileNames;
private final List<Library> mavenOnlyFiles;
public ResolvedInstance(Version version, String gameVersion, Library mainJar, List<String> jarModFileNames, List<Library> mavenOnlyFiles) {
this.version = version;
this.gameVersion = gameVersion;
this.mainJar = mainJar;
this.jarModFileNames = jarModFileNames;
this.mavenOnlyFiles = mavenOnlyFiles;
}
public Version getVersion() {
return version;
}
public String getGameVersion() {
return gameVersion;
}
public Library getMainJar() {
return mainJar;
}
public List<String> getJarModFileNames() {
return jarModFileNames;
}
public List<Library> getMavenOnlyFiles() {
return mavenOnlyFiles;
}
}
/**
* <p>Core methods transforming MultiMCModpack to Official Version Scheme.</p>
*
* <p>Mose of the information can be transformed in a lossless manner, except for some inputs.
* See to do marks below for more information</p>
*
* @param patches List of all Json-Patch.
* @param versionID the version ID. Used when constructing a Version.
* @return The resolved instance.
*/
public static ResolvedInstance resolveArtifact(List<MultiMCInstancePatch> patches, String versionID) {
if (patches.isEmpty()) {
throw new IllegalArgumentException("Empty components.");
}
for (MultiMCInstancePatch patch : patches) {
if (patch.getFormatVersion() != 1) {
throw new UnsupportedOperationException(
String.format("Unsupported JSON-Patch[%s] format version: %d", patch.getID(), patch.getFormatVersion())
);
}
}
StringBuilder message = new StringBuilder();
List<String> minecraftArguments;
ArrayList<Argument> jvmArguments = new ArrayList<>(Arguments.DEFAULT_JVM_ARGUMENTS);
String mainClass;
AssetIndexInfo assetIndex;
int[] javaMajors;
Library mainJar;
List<String> traits;
List<String> tweakers;
List<Library> libraries;
List<Library> mavenOnlyFiles;
List<String> jarModFileNames;
{
MultiMCInstancePatch last = patches.get(patches.size() - 1);
minecraftArguments = last.getMinecraftArguments() == null ? null : StringUtils.tokenize(last.getMinecraftArguments());
mainClass = last.getMainClass();
assetIndex = last.getAssetIndex();
javaMajors = last.getJavaMajors();
mainJar = last.getMainJar();
traits = last.getTraits();
tweakers = last.getTweakers();
libraries = last.getLibraries();
mavenOnlyFiles = last.getMavenOnlyFiles();
jarModFileNames = last.getJarMods().stream().map(Library::getFileName).collect(Collectors.toList());
}
for (int i = patches.size() - 2; i >= 0; i--) {
MultiMCInstancePatch patch = patches.get(i);
if (minecraftArguments == null & patch.getMinecraftArguments() != null) {
minecraftArguments = StringUtils.tokenize(patch.getMinecraftArguments());
}
for (String jvmArg : patch.getJvmArgs()) {
jvmArguments.add(new StringArgument(jvmArg));
}
mainClass = Lang.requireNonNullElse(mainClass, patch.getMainClass());
assetIndex = Lang.requireNonNullElse(patch.getAssetIndex(), assetIndex);
javaMajors = Lang.requireNonNullElse(patch.getJavaMajors(), javaMajors);
mainJar = Lang.requireNonNullElse(patch.getMainJar(), mainJar);
traits = Lang.merge(patch.getTraits(), traits);
tweakers = Lang.merge(patch.getTweakers(), tweakers);
libraries = Lang.merge(patch.getLibraries(), libraries);
mavenOnlyFiles = Lang.merge(patch.getMavenOnlyFiles(), mavenOnlyFiles);
jarModFileNames = Lang.merge(patch.getJarMods().stream().map(Library::getFileName).collect(Collectors.toList()), jarModFileNames);
}
mainClass = Lang.requireNonNullElse(mainClass, "net.minecraft.client.Minecraft");
if (minecraftArguments == null) {
minecraftArguments = new ArrayList<>();
}
// '--tweakClass' can't be the last argument.
for (int i = minecraftArguments.size() - 2; i >= 0; i--) {
if ("--tweakClass".equals(minecraftArguments.get(i))) {
tweakers.add(minecraftArguments.get(i + 1));
minecraftArguments.remove(i);
minecraftArguments.remove(i);
}
}
traits = dropDuplicate(traits, Function.identity());
tweakers = dropDuplicate(tweakers, Function.identity());
libraries = dropDuplicate(libraries, Library::getName);
jarModFileNames = dropDuplicate(jarModFileNames, Function.identity());
for (String tweaker : tweakers) {
minecraftArguments.add("--tweakClass");
minecraftArguments.add(tweaker);
}
for (String trait : traits) {
switch (trait) {
case "FirstThreadOnMacOS": {
jvmArguments.add(new RuledArgument(
Collections.singletonList(
new CompatibilityRule(CompatibilityRule.Action.ALLOW, new OSRestriction(OperatingSystem.MACOS))
),
Collections.singletonList("-XstartOnFirstThread")
));
break;
}
case "XR:Initial": // Flag for chat report. See https://discord.com/channels/132965178051526656/134843027553255425/1380885829702127616
case "texturepacks": // HMCL hasn't support checking whether a game version supports texture packs.
case "no-texturepacks": {
break;
}
default: {
message.append(" - Trait: ").append(trait).append('\n');
break;
}
}
}
for (Library library : libraries) {
Artifact artifact = library.getArtifact();
if ("io.github.zekerzhayard".equals(artifact.getGroup()) && "ForgeWrapper".equals(artifact.getName())) {
jvmArguments.add(new StringArgument("-Dforgewrapper.librariesDir=${library_directory}"));
jvmArguments.add(new StringArgument("-Dforgewrapper.minecraft=${primary_jar}"));
for (Library lib : libraries) {
Artifact ar = lib.getArtifact();
if ("net.neoforged".equals(ar.getGroup()) && "neoforge".equals(ar.getName()) && "installer".equals(ar.getClassifier()) ||
"net.minecraftforge".equals(ar.getGroup()) && "forge".equals(ar.getName()) && "installer".equals(ar.getClassifier())
) {
jvmArguments.add(new StringArgument("-Dforgewrapper.installer=${library_directory}/" + ar.getPath()));
}
}
}
}
Version version = new Version(versionID)
.setArguments(new Arguments().addGameArguments(minecraftArguments).addJVMArgumentsDirect(jvmArguments))
.setMainClass(mainClass)
.setLibraries(libraries)
.setAssetIndex(assetIndex)
.setDownload(new JsonMap<>(Collections.singletonMap(DownloadType.CLIENT, mainJar.getRawDownloadInfo())));
/* TODO: Official Version-Json can only store one pre-defined GameJavaVersion, including 8, 11, 16, 17 and 21.
An array of all suitable java versions are NOT supported.
For compatibility with official launcher and any other launchers, a transform is made between int[] and GameJavaVersion. */
javaMajors:
if (javaMajors != null) {
javaMajors = javaMajors.clone();
Arrays.sort(javaMajors);
for (int i = javaMajors.length - 1; i >= 0; i--) {
GameJavaVersion jv = GameJavaVersion.get(javaMajors[i]);
if (jv != null) {
version = version.setJavaVersion(jv);
break javaMajors;
}
}
message.append(" - Java Version Range: ").append(Arrays.toString(javaMajors)).append('\n');
}
version = version.markAsResolved();
String gameVersion = null;
for (MultiMCInstancePatch patch : patches) {
if (MultiMCComponents.getComponent(patch.getID()) == LibraryAnalyzer.LibraryType.MINECRAFT) {
gameVersion = patch.getVersion();
break;
}
}
if (message.length() != 0) {
if (message.charAt(message.length() - 1) == '\n') {
message.setLength(message.length() - 1);
}
Logger.LOG.warning("Cannot fully parse MultiMC modpack with following unsupported features: \n" + message);
}
return new ResolvedInstance(version, gameVersion, mainJar, jarModFileNames, mavenOnlyFiles);
}
}

View File

@ -31,6 +31,7 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -74,17 +75,16 @@ public class MultiMCModpackExportTask extends Task<Void> {
.orElseThrow(() -> new IOException("Cannot parse the version of " + versionId));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId), gameVersion);
List<MultiMCManifest.MultiMCManifestComponent> components = new ArrayList<>();
components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, "net.minecraft", gameVersion));
analyzer.getVersion(FORGE).ifPresent(forgeVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.minecraftforge", forgeVersion)));
analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.neoforged", neoForgeVersion)));
analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion)));
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "org.quiltmc.quilt-loader", quiltVersion)));
components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, MultiMCComponents.getComponent(MINECRAFT), gameVersion));
for (Map.Entry<String, LibraryAnalyzer.LibraryType> pair : MultiMCComponents.getPairs()) {
if (pair.getValue().isModLoader()) {
analyzer.getVersion(pair.getValue()).ifPresent(
v -> components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, pair.getKey(), v))
);
}
}
MultiMCManifest mmcPack = new MultiMCManifest(1, components);
zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json");

View File

@ -19,21 +19,26 @@ package org.jackhuang.hmcl.mod.multimc;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
@ -41,79 +46,47 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Objects;
import java.util.stream.Collectors;
/**
*
* @author huangyuhui
* <p>A task transforming MultiMC Modpack Scheme to Official Launcher Scheme.
* The transforming process contains 7 stage:
* General Setup, Load Components, Resolve Json-Patch, Build Artifact,
* Copy Embedded Files, Assemble Game, Download Game and Apply JAR mods.
* See codes below for detailed implementation.
* </p>
*/
public final class MultiMCModpackInstallTask extends Task<Void> {
public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.ResolvedInstance> {
private final File zipFile;
private final Modpack modpack;
private final MultiMCInstanceConfiguration manifest;
private final String name;
private final DefaultGameRepository repository;
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
private final List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
private final List<Task<?>> dependents = new ArrayList<>();
private final List<Task<?>> dependencies = new ArrayList<>();
private final DefaultDependencyManager dependencyManager;
public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, String name) {
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
this.name = name;
this.dependencyManager = dependencyManager;
this.repository = dependencyManager.getGameRepository();
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion());
if (manifest.getMmcPack() != null) {
Optional<MultiMCManifest.MultiMCManifestComponent> forge = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.minecraftforge")).findAny();
forge.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("forge", c.getVersion());
});
Optional<MultiMCManifest.MultiMCManifestComponent> neoForge = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.neoforged")).findAny();
neoForge.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("neoforge", c.getVersion());
});
Optional<MultiMCManifest.MultiMCManifestComponent> liteLoader = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("com.mumfrey.liteloader")).findAny();
liteLoader.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("liteloader", c.getVersion());
});
Optional<MultiMCManifest.MultiMCManifestComponent> fabric = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.fabricmc.fabric-loader")).findAny();
fabric.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("fabric", c.getVersion());
});
Optional<MultiMCManifest.MultiMCManifestComponent> quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("org.quiltmc.quilt-loader")).findAny();
quilt.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("quilt", c.getVersion());
});
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
if (event.isFailed())
repository.removeVersionFromDisk(name);
});
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public boolean doPreExecute() {
return true;
@ -121,85 +94,85 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
@Override
public void preExecute() throws Exception {
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
// Stage #0: General Setup
{
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
ModpackConfiguration<MultiMCInstanceConfiguration> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class));
ModpackConfiguration<MultiMCInstanceConfiguration> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class));
if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version.");
if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
} catch (JsonParseException | IOException ignore) {
String mcDirectory;
try (FileSystem fs = openModpack()) {
mcDirectory = getRootPath(fs).resolve(".minecraft").toAbsolutePath().normalize().toString();
}
// TODO: Optimize unbearably slow ModpackInstallTask
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(mcDirectory), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
String subDirectory;
// Stage #1: Load all related Json-Patch from meta maven or local mod pack.
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(modpack.getEncoding()).build()) {
// /.minecraft
if (Files.exists(fs.getPath("/.minecraft"))) {
subDirectory = "/.minecraft";
// /minecraft
} else if (Files.exists(fs.getPath("/minecraft"))) {
subDirectory = "/minecraft";
// /[name]/.minecraft
} else if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft"))) {
subDirectory = "/" + manifest.getName() + "/.minecraft";
// /[name]/minecraft
} else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft"))) {
subDirectory = "/" + manifest.getName() + "/minecraft";
} else {
subDirectory = "/" + manifest.getName() + "/.minecraft";
try (FileSystem fs = openModpack()) {
Path root = getRootPath(fs);
for (MultiMCManifest.MultiMCManifestComponent component : Objects.requireNonNull(
Objects.requireNonNull(manifest.getMmcPack(), "mmc-pack.json").getComponents(), "components"
)) {
String componentID = Objects.requireNonNull(component.getUid(), "Component ID");
Path patchPath = root.resolve(String.format("patches/%s.json", componentID));
Task<String> task;
if (Files.exists(patchPath)) {
if (!Files.isRegularFile(patchPath)) {
throw new IllegalArgumentException("Json-Patch isn't a file: " + componentID);
}
// TODO: Task.completed has unclear compatibility issue.
String text = FileUtils.readText(patchPath, StandardCharsets.UTF_8);
task = Task.supplyAsync(() -> text);
} else {
task = new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion()));
}
Task<MultiMCInstancePatch> task2 = task.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
patches.add(task2);
dependents.add(task2);
}
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(subDirectory), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(subDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
public List<Task<?>> getDependents() {
// Stage #2: Resolve all Json-Patch
return dependents;
}
@Override
public void execute() throws Exception {
Version version = repository.readVersionJson(name);
// Stage #3: Build Json-Patch artifact.
MultiMCInstancePatch.ResolvedInstance artifact = MultiMCInstancePatch.resolveArtifact(patches.stream()
.map(value -> Objects.requireNonNull(value.getResult(), "MultiMCInstancePatch"))
.collect(Collectors.toList()), name
);
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
Path root = MultiMCModpackProvider.getRootPath(fs.getPath("/"));
Path patches = root.resolve("patches");
if (Files.exists(patches)) {
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(patches)) {
for (Path patchJson : directoryStream) {
if (patchJson.toString().endsWith(".json")) {
// If json is malformed, we should stop installing this modpack instead of skipping it.
MultiMCInstancePatch multiMCPatch = JsonUtils.GSON.fromJson(FileUtils.readText(patchJson), MultiMCInstancePatch.class);
List<String> arguments = new ArrayList<>();
for (String arg : multiMCPatch.getTweakers()) {
arguments.add("--tweakClass");
arguments.add(arg);
}
Version patch = new Version(multiMCPatch.getName(), multiMCPatch.getVersion(), 1, new Arguments().addGameArguments(arguments), multiMCPatch.getMainClass(), multiMCPatch.getLibraries());
version = version.addPatch(patch);
}
}
}
}
// Stage #4: Copy embedded files.
try (FileSystem fs = openModpack()) {
Path root = getRootPath(fs);
Path libraries = root.resolve("libraries");
if (Files.exists(libraries))
FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries"));
Path jarmods = root.resolve("jarmods");
if (Files.exists(jarmods))
FileUtils.copyDirectory(jarmods, repository.getVersionRoot(name).toPath().resolve("jarmods"));
String iconKey = this.manifest.getIconKey();
if (iconKey != null) {
Path iconFile = root.resolve(iconKey + ".png");
@ -209,6 +182,95 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
}
}
dependencies.add(repository.saveAsync(version));
// Stage #5: Assemble game files.
{
Version version = artifact.getVersion();
dependencies.add(repository.saveAsync(artifact.getVersion()));
dependencies.add(new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true));
dependencies.add(new GameLibrariesTask(
dependencyManager,
// TODO: check integrity of maven-only files when launching games?
version.setLibraries(Lang.merge(version.getLibraries(), artifact.getMavenOnlyFiles())),
true
));
Artifact mainJarArtifact = artifact.getMainJar().getArtifact();
String gameVersion = artifact.getGameVersion();
if (gameVersion != null &&
"com.mojang".equals(mainJarArtifact.getGroup()) &&
"minecraft".equals(mainJarArtifact.getName()) &&
Objects.equals(gameVersion, mainJarArtifact.getVersion()) &&
"client".equals(mainJarArtifact.getClassifier())
) {
dependencies.add(new GameDownloadTask(dependencyManager, gameVersion, version));
} else {
dependencies.add(new GameDownloadTask(dependencyManager, null, version));
}
}
setResult(artifact);
}
@Override
public List<Task<?>> getDependencies() {
// Stage #6: Download game files.
return dependencies;
}
@Override
public boolean doPostExecute() {
return true;
}
@Override
public void postExecute() throws Exception {
MultiMCInstancePatch.ResolvedInstance artifact = Objects.requireNonNull(getResult(), "ResolvedInstance");
List<String> files = artifact.getJarModFileNames();
if (!isDependenciesSucceeded() || files.isEmpty()) {
return;
}
// Stage #7: Apply jar mods.
try (FileSystem fs = openModpack()) {
Path root = getRootPath(fs).resolve("jarmods");
try (FileSystem mc = CompressingUtils.writable(
repository.getVersionRoot(name).toPath().resolve(name + ".jar")
).setAutoDetectEncoding(true).build()) {
for (String fileName : files) {
try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).setAutoDetectEncoding(true).build()) {
FileUtils.copyDirectory(jm.getPath("/"), mc.getPath("/"));
}
}
}
}
}
private FileSystem openModpack() throws IOException {
return CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).setEncoding(modpack.getEncoding()).build();
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
private static Path getRootPath(FileSystem fs) throws IOException {
Path root = fs.getPath("/");
if (testPath(root)) {
return root;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
for (Path candidate : stream) {
if (testPath(candidate)) {
return candidate;
}
}
}
throw new IOException("Not a valid MultiMC modpack");
}
}

View File

@ -31,9 +31,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public final class MultiMCModpackProvider implements ModpackProvider {
public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider();
@ -56,20 +54,6 @@ public final class MultiMCModpackProvider implements ModpackProvider {
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
public static Path getRootPath(Path root) throws IOException {
if (testPath(root)) return root;
try (Stream<Path> stream = Files.list(root)) {
Path candidate = stream.filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (testPath(candidate)) return candidate;
throw new IOException("Not a valid MultiMC modpack");
}
}
private static String getRootEntryName(ZipArchiveReader file) throws IOException {
final String instanceFileName = "instance.cfg";