Support install Forge/OptiFine from local file

This commit is contained in:
huanghongxun
2019-04-30 20:26:31 +08:00
parent 6595e0a3cf
commit 5e659352d7
12 changed files with 285 additions and 23 deletions

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -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.
*