mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-09 11:55:52 -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 Version version;
|
||||
private final List<Library> libraries;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
/**
|
||||
@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
|
||||
* @param version the <b>resolved</b> 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.version = version;
|
||||
this.libraries = libraries;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
@Override
|
||||
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);
|
||||
if (!file.exists())
|
||||
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 org.jackhuang.hmcl.event.*;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* An implementation of classic Minecraft game repository.
|
||||
*
|
||||
@ -82,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
||||
}
|
||||
|
||||
public Path getArtifactFile(Version version, Artifact artifact) {
|
||||
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
return getBaseDirectory();
|
||||
@ -265,10 +269,8 @@ public class DefaultGameRepository implements GameRepository {
|
||||
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
||||
return;
|
||||
|
||||
Schedulers.newThread().schedule(() -> {
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
});
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Collection;
|
||||
@ -24,8 +26,6 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@ -128,6 +128,10 @@ public final class StringUtils {
|
||||
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) {
|
||||
return removeSurrounding(str, delimiter, delimiter);
|
||||
}
|
||||
|
@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
|
||||
*/
|
||||
public interface ExceptionalFunction<T, R, E extends Exception> {
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
copyFile(srcFile, destFile);
|
||||
srcFile.delete();
|
||||
|
Loading…
x
Reference in New Issue
Block a user