Support install Forge/OptiFine from local file
This commit is contained in:
@@ -31,6 +31,9 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Note: This class has no state.
|
||||
*
|
||||
@@ -112,8 +115,29 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
.thenCompose(newVersion -> new VersionJsonSaveTask(repository, newVersion));
|
||||
}
|
||||
|
||||
|
||||
public ExceptionalFunction<Version, TaskResult<Version>, ?> installLibraryAsync(RemoteVersion libraryVersion) {
|
||||
return version -> installLibraryAsync(version, libraryVersion);
|
||||
}
|
||||
|
||||
public Task installLibraryAsync(Version oldVersion, Path installer) {
|
||||
return Task
|
||||
.of(() -> {
|
||||
})
|
||||
.thenCompose(() -> {
|
||||
try {
|
||||
return ForgeInstallTask.install(this, oldVersion, installer);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return OptiFineInstallTask.install(this, oldVersion, installer);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Library cannot be recognized");
|
||||
})
|
||||
.thenCompose(LibrariesUniqueTask::new)
|
||||
.thenCompose(MaintainTask::new)
|
||||
.thenCompose(newVersion -> new VersionJsonSaveTask(repository, newVersion));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
public class VersionMismatchException extends Exception {
|
||||
|
||||
private final String expect, actual;
|
||||
|
||||
public VersionMismatchException(String expect, String actual) {
|
||||
super("Mismatched game version requirement, library requires game to be " + expect + ", but actual is " + actual);
|
||||
this.expect = expect;
|
||||
this.actual = actual;
|
||||
}
|
||||
|
||||
public String getExpect() {
|
||||
return expect;
|
||||
}
|
||||
|
||||
public String getActual() {
|
||||
return actual;
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,26 @@
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.VersionMismatchException;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -90,4 +99,36 @@ public final class ForgeInstallTask extends TaskResult<Version> {
|
||||
else
|
||||
dependency = new ForgeOldInstallTask(dependencyManager, version, installer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Forge library from existing local file.
|
||||
*
|
||||
* @param dependencyManager game repository
|
||||
* @param version version.json
|
||||
* @param installer the Forge installer, either the new or old one.
|
||||
* @return the task to install library
|
||||
* @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want.
|
||||
* @throws VersionMismatchException if required game version of installer does not match the actual one.
|
||||
*/
|
||||
public static TaskResult<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(dependencyManager.getGameRepository().getVersionJar(version));
|
||||
if (!gameVersion.isPresent()) throw new IOException();
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
String installProfileText = FileUtils.readText(fs.getPath("install_profile.json"));
|
||||
Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
|
||||
if (installProfile.containsKey("spec")) {
|
||||
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, installer);
|
||||
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
|
||||
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
|
||||
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
|
||||
throw new VersionMismatchException(profile.getInstall().getMinecraft(), gameVersion.get());
|
||||
return new ForgeOldInstallTask(dependencyManager, version, installer);
|
||||
} else {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.game.Artifact;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
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;
|
||||
@@ -29,7 +32,7 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Immutable
|
||||
public class ForgeNewInstallProfile {
|
||||
public class ForgeNewInstallProfile implements Validation {
|
||||
|
||||
private final int spec;
|
||||
private final String minecraft;
|
||||
@@ -97,6 +100,8 @@ public class ForgeNewInstallProfile {
|
||||
|
||||
/**
|
||||
* Data for processors.
|
||||
*
|
||||
* @return a mutable data map for processors.
|
||||
*/
|
||||
public Map<String, String> getData() {
|
||||
if (data == null)
|
||||
@@ -105,7 +110,13 @@ public class ForgeNewInstallProfile {
|
||||
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
|
||||
}
|
||||
|
||||
public static class Processor {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (minecraft == null || json == null || path == null)
|
||||
throw new JsonParseException("ForgeNewInstallProfile is malformed");
|
||||
}
|
||||
|
||||
public static class Processor implements Validation {
|
||||
private final List<String> sides;
|
||||
private final Artifact jar;
|
||||
private final List<Artifact> classpath;
|
||||
@@ -170,6 +181,12 @@ public class ForgeNewInstallProfile {
|
||||
public Map<String, String> getOutputs() {
|
||||
return outputs == null ? Collections.emptyMap() : outputs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (jar == null)
|
||||
throw new JsonParseException("Processor::jar cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Datum {
|
||||
|
||||
@@ -18,19 +18,26 @@
|
||||
package org.jackhuang.hmcl.download.optifine;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.download.VersionMismatchException;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||
import org.jenkinsci.constant_pool_scanner.Utf8Constant;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.getOrDefault;
|
||||
|
||||
/**
|
||||
* <b>Note</b>: OptiFine should be installed in the end.
|
||||
@@ -42,13 +49,19 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final OptiFineRemoteVersion remote;
|
||||
private final Path installer;
|
||||
private final List<Task> dependents = new LinkedList<>();
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion) {
|
||||
this(dependencyManager, version, remoteVersion, null);
|
||||
}
|
||||
|
||||
public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion, Path installer) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.remote = remoteVersion;
|
||||
this.installer = installer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +80,7 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
public void execute() throws IOException {
|
||||
if (!Arrays.asList("net.minecraft.client.main.Main",
|
||||
"net.minecraft.launchwrapper.Launch")
|
||||
.contains(version.getMainClass()))
|
||||
@@ -82,6 +95,10 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
remote.getUrl()))
|
||||
);
|
||||
|
||||
if (installer != null) {
|
||||
FileUtils.copyFile(installer, dependencyManager.getGameRepository().getLibraryFile(version, library).toPath());
|
||||
}
|
||||
|
||||
List<Library> libraries = new LinkedList<>();
|
||||
libraries.add(library);
|
||||
|
||||
@@ -99,4 +116,37 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
|
||||
public static class UnsupportedOptiFineInstallationException extends UnsupportedOperationException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install OptiFine library from existing local file.
|
||||
*
|
||||
* @param dependencyManager game repository
|
||||
* @param version version.json
|
||||
* @param installer the OptiFine installer
|
||||
* @return the task to install library
|
||||
* @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want.
|
||||
* @throws VersionMismatchException if required game version of installer does not match the actual one.
|
||||
*/
|
||||
public static TaskResult<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
|
||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(jar);
|
||||
if (!gameVersion.isPresent()) throw new IOException();
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(fs.getPath("Config.class")), ConstantType.UTF8);
|
||||
List<String> constants = new ArrayList<>();
|
||||
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
|
||||
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
|
||||
String ofEdition = getOrDefault(constants, constants.indexOf("OF_EDITION") + 1, null);
|
||||
String ofRelease = getOrDefault(constants, constants.indexOf("OF_RELEASE") + 1, null);
|
||||
|
||||
if (mcVersion == null || ofEdition == null || ofRelease == null)
|
||||
throw new IOException("Unrecognized OptiFine installer");
|
||||
|
||||
if (!mcVersion.equals(gameVersion.get()))
|
||||
throw new VersionMismatchException(mcVersion, gameVersion.get());
|
||||
|
||||
return new OptiFineInstallTask(dependencyManager, version,
|
||||
new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, () -> null, false), installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ public final class Lang {
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T getOrDefault(List<T> a, int index, T defaultValue) {
|
||||
return index < 0 || index >= a.size() ? defaultValue : a.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join two collections into one list.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user