mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-09 20:06:39 -04:00
Support automatic installation of Forge 1.13.
This commit is contained in:
parent
426ea607e8
commit
e8f5088049
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* 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.forge;
|
||||||
|
|
||||||
|
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 java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
public class ForgeNewInstallProfile {
|
||||||
|
|
||||||
|
private final int spec;
|
||||||
|
private final String minecraft;
|
||||||
|
private final String json;
|
||||||
|
private final List<Library> libraries;
|
||||||
|
private final List<Processor> processors;
|
||||||
|
private final Map<String, Datum> data;
|
||||||
|
|
||||||
|
public ForgeNewInstallProfile(int spec, String minecraft, String json, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
|
||||||
|
this.spec = spec;
|
||||||
|
this.minecraft = minecraft;
|
||||||
|
this.json = json;
|
||||||
|
this.libraries = libraries;
|
||||||
|
this.processors = processors;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specification for install_profile.json.
|
||||||
|
*/
|
||||||
|
public int getSpec() {
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vanilla game version that this installer supports.
|
||||||
|
*/
|
||||||
|
public String getMinecraft() {
|
||||||
|
return minecraft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version json to be installed.
|
||||||
|
* @return path of the version json relative to the installer JAR file.
|
||||||
|
*/
|
||||||
|
public String getJson() {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Libraries that processors depend on.
|
||||||
|
* @return the required dependencies.
|
||||||
|
*/
|
||||||
|
public List<Library> getLibraries() {
|
||||||
|
return libraries == null ? Collections.emptyList() : libraries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tasks to be executed to setup modded environment.
|
||||||
|
*/
|
||||||
|
public List<Processor> getProcessors() {
|
||||||
|
if (processors == null) return Collections.emptyList();
|
||||||
|
return processors.stream().filter(p -> p.isSide("client")).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for processors.
|
||||||
|
*/
|
||||||
|
public Map<String, String> getData() {
|
||||||
|
if (data == null)
|
||||||
|
return new HashMap<>();
|
||||||
|
|
||||||
|
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Processor {
|
||||||
|
private final List<String> sides;
|
||||||
|
private final Artifact jar;
|
||||||
|
private final List<Artifact> classpath;
|
||||||
|
private final List<String> args;
|
||||||
|
private final Map<String, String> outputs;
|
||||||
|
|
||||||
|
public Processor(List<String> sides, Artifact jar, List<Artifact> classpath, List<String> args, Map<String, String> outputs) {
|
||||||
|
this.sides = sides;
|
||||||
|
this.jar = jar;
|
||||||
|
this.classpath = classpath;
|
||||||
|
this.args = args;
|
||||||
|
this.outputs = outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check which side this processor should be run on. We only support client install currently.
|
||||||
|
* @param side can be one of "client", "server", "extract".
|
||||||
|
* @return true if the processor can run on the side.
|
||||||
|
*/
|
||||||
|
public boolean isSide(String side) {
|
||||||
|
return sides == null || sides.contains(side);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The executable jar of this processor task. Will be executed in installation process.
|
||||||
|
* @return the artifact path of executable jar.
|
||||||
|
*/
|
||||||
|
public Artifact getJar() {
|
||||||
|
return jar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dependencies of this processor task.
|
||||||
|
* @return the artifact path of dependencies.
|
||||||
|
*/
|
||||||
|
public List<Artifact> getClasspath() {
|
||||||
|
return classpath == null ? Collections.emptyList() : classpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arguments to pass to the processor jar.
|
||||||
|
* Each item can be in one of the following formats:
|
||||||
|
* [artifact]: An artifact path, used for locating files.
|
||||||
|
* {entry}: Get corresponding value of the entry in {@link ForgeNewInstallProfile#getData()}
|
||||||
|
* {MINECRAFT_JAR}: path of the Minecraft jar.
|
||||||
|
* {SIDE}: values other than "client" will be ignored.
|
||||||
|
* @return arguments to pass to the processor jar.
|
||||||
|
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||||
|
*/
|
||||||
|
public List<String> getArgs() {
|
||||||
|
return args == null ? Collections.emptyList() : args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File-checksum pairs, used for verifying the output file is correct.
|
||||||
|
* Arguments to pass to the processor jar.
|
||||||
|
* Keys can be in one of [artifact] or {entry}. Should be file path.
|
||||||
|
* Values can be in one of {entry} or 'literal'. Should be SHA-1 checksum.
|
||||||
|
* @return files output from this processor.
|
||||||
|
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||||
|
*/
|
||||||
|
public Map<String, String> getOutputs() {
|
||||||
|
return outputs == null ? Collections.emptyMap() : outputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Datum {
|
||||||
|
private final String client;
|
||||||
|
|
||||||
|
public Datum(String client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be in the following formats:
|
||||||
|
* [value]: An artifact path.
|
||||||
|
* 'value': A string literal.
|
||||||
|
* value: A file in the installer package, to be extracted to a temp folder, and then have the absolute path in replacements.
|
||||||
|
* @return Value to use for the client install
|
||||||
|
*/
|
||||||
|
public String getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,249 @@
|
|||||||
|
package org.jackhuang.hmcl.download.forge;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||||
|
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||||
|
import org.jackhuang.hmcl.game.*;
|
||||||
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.task.TaskResult;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||||
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||||
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||||
|
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
|
public class ForgeNewInstallTask extends TaskResult<Version> {
|
||||||
|
|
||||||
|
private final DefaultDependencyManager dependencyManager;
|
||||||
|
private final DefaultGameRepository gameRepository;
|
||||||
|
private final Version version;
|
||||||
|
private final Path installer;
|
||||||
|
private final List<Task> dependents = new LinkedList<>();
|
||||||
|
private final List<Task> dependencies = new LinkedList<>();
|
||||||
|
|
||||||
|
private ForgeNewInstallProfile profile;
|
||||||
|
private Version forgeVersion;
|
||||||
|
|
||||||
|
public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
|
||||||
|
this.dependencyManager = dependencyManager;
|
||||||
|
this.gameRepository = dependencyManager.getGameRepository();
|
||||||
|
this.version = version;
|
||||||
|
this.installer = installer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
|
||||||
|
if (StringUtils.isSurrounded(literal, "{", "}"))
|
||||||
|
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
|
||||||
|
else if (StringUtils.isSurrounded(literal, "'", "'"))
|
||||||
|
return StringUtils.removeSurrounding(literal, "'");
|
||||||
|
else if (StringUtils.isSurrounded(literal, "[", "]"))
|
||||||
|
return gameRepository.getArtifactFile(version, new Artifact(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
|
||||||
|
else
|
||||||
|
return plainConverter.apply(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Task> getDependents() {
|
||||||
|
return dependents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Task> getDependencies() {
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "version";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doPreExecute() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preExecute() throws Exception {
|
||||||
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||||
|
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
|
||||||
|
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
|
||||||
|
|
||||||
|
for (Library library : profile.getLibraries()) {
|
||||||
|
Path file = fs.getPath("maven").resolve(library.getPath());
|
||||||
|
if (Files.exists(file)) {
|
||||||
|
Path dest = gameRepository.getLibraryFile(version, library).toPath();
|
||||||
|
FileUtils.copyFile(file, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws Exception {
|
||||||
|
Path temp = Files.createTempDirectory("forge_installer");
|
||||||
|
int finished = 0;
|
||||||
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||||
|
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
|
||||||
|
Map<String, String> data = profile.getData();
|
||||||
|
|
||||||
|
updateProgress(0, processors.size());
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : data.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
|
||||||
|
data.put(key, parseLiteral(value,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
str -> {
|
||||||
|
Path dest = temp.resolve(str);
|
||||||
|
FileUtils.copyFile(fs.getPath(str), dest);
|
||||||
|
return dest.toString();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("SIDE", "client");
|
||||||
|
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
|
||||||
|
|
||||||
|
for (ForgeNewInstallProfile.Processor processor : processors) {
|
||||||
|
Map<String, String> outputs = new HashMap<>();
|
||||||
|
boolean miss = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
|
||||||
|
key = parseLiteral(key, data, ExceptionalFunction.identity());
|
||||||
|
value = parseLiteral(value, data, ExceptionalFunction.identity());
|
||||||
|
|
||||||
|
if (key == null || value == null) {
|
||||||
|
throw new Exception("Invalid forge installation configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.put(key, value);
|
||||||
|
|
||||||
|
Path artifact = Paths.get(key);
|
||||||
|
if (Files.exists(artifact)) {
|
||||||
|
String code;
|
||||||
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||||
|
code = encodeHex(digest("SHA-1", stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.equals(code, value)) {
|
||||||
|
Files.delete(artifact);
|
||||||
|
LOG.info("Found existing file is not valid: " + artifact);
|
||||||
|
|
||||||
|
miss = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
miss = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processor.getOutputs().isEmpty() && !miss) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
|
||||||
|
if (!Files.isRegularFile(jar))
|
||||||
|
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
|
||||||
|
|
||||||
|
String mainClass;
|
||||||
|
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
||||||
|
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(mainClass))
|
||||||
|
throw new Exception("Game processor jar does not have main class " + jar);
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
|
||||||
|
command.add("-cp");
|
||||||
|
|
||||||
|
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
|
||||||
|
for (Artifact artifact : processor.getClasspath()) {
|
||||||
|
Path file = gameRepository.getArtifactFile(version, artifact);
|
||||||
|
if (!Files.isRegularFile(file))
|
||||||
|
throw new Exception("Game processor dependency missing");
|
||||||
|
classpath.add(file.toString());
|
||||||
|
}
|
||||||
|
classpath.add(jar.toString());
|
||||||
|
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
||||||
|
|
||||||
|
command.add(mainClass);
|
||||||
|
|
||||||
|
List<String> args = processor.getArgs().stream().map(arg -> {
|
||||||
|
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
|
||||||
|
if (parsed == null)
|
||||||
|
throw new IllegalStateException("Invalid forge installation configuration");
|
||||||
|
return parsed;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
command.addAll(args);
|
||||||
|
|
||||||
|
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
|
||||||
|
Process process = new ProcessBuilder(command).start();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
|
for (String line; (line = reader.readLine()) != null;) {
|
||||||
|
System.out.println(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new IllegalStateException("Game processor exited abnormally");
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : outputs.entrySet()) {
|
||||||
|
Path artifact = Paths.get(entry.getKey());
|
||||||
|
if (!Files.isRegularFile(artifact))
|
||||||
|
throw new FileNotFoundException("File missing: " + artifact);
|
||||||
|
|
||||||
|
String code;
|
||||||
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||||
|
code = encodeHex(digest("SHA-1", stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Objects.equals(code, entry.getValue())) {
|
||||||
|
Files.delete(artifact);
|
||||||
|
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(++finished, processors.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve the version
|
||||||
|
SimpleVersionProvider provider = new SimpleVersionProvider();
|
||||||
|
provider.addVersion(version);
|
||||||
|
|
||||||
|
setResult(forgeVersion
|
||||||
|
.setInheritsFrom(version.getId())
|
||||||
|
.resolve(provider).setJar(null)
|
||||||
|
.setId(version.getId()).setLogging(Collections.emptyMap()));
|
||||||
|
|
||||||
|
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
|
||||||
|
|
||||||
|
FileUtils.deleteDirectory(temp.toFile());
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,7 @@ public final class GameLibrariesTask extends Task {
|
|||||||
|
|
||||||
private final AbstractDependencyManager dependencyManager;
|
private final AbstractDependencyManager dependencyManager;
|
||||||
private final Version version;
|
private final Version version;
|
||||||
|
private final List<Library> libraries;
|
||||||
private final List<Task> dependencies = new LinkedList<>();
|
private final List<Task> dependencies = new LinkedList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
|
|||||||
* @param version the <b>resolved</b> version
|
* @param version the <b>resolved</b> version
|
||||||
*/
|
*/
|
||||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||||
|
this(dependencyManager, version, version.getLibraries());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||||
|
* @param version the <b>resolved</b> version
|
||||||
|
*/
|
||||||
|
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
|
this.libraries = libraries;
|
||||||
|
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||||
|
97
HMCLCore/src/main/java/org/jackhuang/hmcl/game/Artifact.java
Normal file
97
HMCLCore/src/main/java/org/jackhuang/hmcl/game/Artifact.java
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@JsonAdapter(Artifact.Serializer.class)
|
||||||
|
public final class Artifact {
|
||||||
|
|
||||||
|
private final String group;
|
||||||
|
private final String name;
|
||||||
|
private final String version;
|
||||||
|
private final String classifier;
|
||||||
|
private final String extension;
|
||||||
|
|
||||||
|
private final String descriptor;
|
||||||
|
private final String fileName;
|
||||||
|
private final String path;
|
||||||
|
|
||||||
|
public Artifact(String descriptor) {
|
||||||
|
this.descriptor = descriptor;
|
||||||
|
|
||||||
|
String[] arr = descriptor.split(":", 4);
|
||||||
|
if (arr.length != 3 && arr.length != 4)
|
||||||
|
throw new IllegalArgumentException("Artifact name is malformed");
|
||||||
|
|
||||||
|
String ext = null;
|
||||||
|
int last = arr.length - 1;
|
||||||
|
String[] splitted = arr[last].split("@");
|
||||||
|
if (splitted.length == 2) {
|
||||||
|
arr[last] = splitted[0];
|
||||||
|
ext = splitted[1];
|
||||||
|
} else if (splitted.length > 2) {
|
||||||
|
throw new IllegalArgumentException("Artifact name is malformed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.group = arr[0].replace("\\", "/");
|
||||||
|
this.name = arr[1];
|
||||||
|
this.version = arr[2];
|
||||||
|
this.classifier = arr.length >= 4 ? arr[3] : null;
|
||||||
|
this.extension = ext == null ? "jar" : ext;
|
||||||
|
|
||||||
|
String fileName = this.name + "-" + this.version;
|
||||||
|
if (classifier != null) fileName += "-" + this.classifier;
|
||||||
|
this.fileName = fileName + "." + this.extension;
|
||||||
|
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGroup() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClassifier() {
|
||||||
|
return classifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtension() {
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getPath(Path root) {
|
||||||
|
return root.resolve(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Serializer implements JsonDeserializer<Artifact>, JsonSerializer<Artifact> {
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Artifact src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Artifact deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return json.isJsonPrimitive() ? new Artifact(json.getAsJsonPrimitive().getAsString()) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,19 +20,19 @@ package org.jackhuang.hmcl.game;
|
|||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.event.*;
|
import org.jackhuang.hmcl.event.*;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
import org.jackhuang.hmcl.mod.ModManager;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of classic Minecraft game repository.
|
* An implementation of classic Minecraft game repository.
|
||||||
*
|
*
|
||||||
@ -82,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Path getArtifactFile(Version version, Artifact artifact) {
|
||||||
|
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File getRunDirectory(String id) {
|
public File getRunDirectory(String id) {
|
||||||
return getBaseDirectory();
|
return getBaseDirectory();
|
||||||
@ -265,10 +269,8 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Schedulers.newThread().schedule(() -> {
|
refreshVersionsImpl();
|
||||||
refreshVersionsImpl();
|
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -24,8 +26,6 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
@ -128,6 +128,10 @@ public final class StringUtils {
|
|||||||
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
|
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isSurrounded(String str, String prefix, String suffix) {
|
||||||
|
return str.startsWith(prefix) && str.endsWith(suffix);
|
||||||
|
}
|
||||||
|
|
||||||
public static String removeSurrounding(String str, String delimiter) {
|
public static String removeSurrounding(String str, String delimiter) {
|
||||||
return removeSurrounding(str, delimiter, delimiter);
|
return removeSurrounding(str, delimiter, delimiter);
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
|
|||||||
*/
|
*/
|
||||||
public interface ExceptionalFunction<T, R, E extends Exception> {
|
public interface ExceptionalFunction<T, R, E extends Exception> {
|
||||||
R apply(T t) throws E;
|
R apply(T t) throws E;
|
||||||
|
|
||||||
|
static <T, E extends RuntimeException> ExceptionalFunction<T, T, E> identity() {
|
||||||
|
return t -> t;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,6 +275,22 @@ public final class FileUtils {
|
|||||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void copyFile(Path srcFile, Path destFile)
|
||||||
|
throws IOException {
|
||||||
|
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||||
|
Objects.requireNonNull(destFile, "Destination must not be null");
|
||||||
|
if (!Files.exists(srcFile))
|
||||||
|
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
|
||||||
|
if (Files.isDirectory(srcFile))
|
||||||
|
throw new IOException("Source '" + srcFile + "' exists but is a directory");
|
||||||
|
Path parentFile = destFile.getParent();
|
||||||
|
Files.createDirectories(parentFile);
|
||||||
|
if (Files.exists(destFile) && !Files.isWritable(destFile))
|
||||||
|
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||||
|
|
||||||
|
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
public static void moveFile(File srcFile, File destFile) throws IOException {
|
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||||
copyFile(srcFile, destFile);
|
copyFile(srcFile, destFile);
|
||||||
srcFile.delete();
|
srcFile.delete();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user