Compare commits

...
12 Commits
19 changed files with 268 additions and 167 deletions

No files matched your search

@@ -383,13 +383,13 @@ public final class LauncherHelper {
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null);
flag = true;
}
// Cannot allocate too much memory exceeding free space.
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null);
flag = true;
}
@@ -401,7 +401,7 @@ public final class LauncherHelper {
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageType.ERROR, onAccept);
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null);
flag = true;
}
}
@@ -150,6 +150,8 @@ public final class LogWindow extends Stage {
private JFXComboBox<String> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
for (String i : cboLines.getItems())
@@ -178,6 +180,7 @@ public final class LogWindow extends Stage {
}
private void onClear() {
impl.listView.getItems().clear();
logs.clear();
}
@@ -279,7 +282,6 @@ public final class LogWindow extends Stage {
prefWidthProperty().bind(clippedContainer.widthProperty());
}
setPadding(new Insets(2));
getStyleClass().add("log");
setWrapText(true);
setGraphic(null);
}
@@ -93,5 +93,15 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnMouseClicked(e -> onClick.run());
return ret;
}
protected abstract List<Node> initializeToolbar(T skinnable);
}
@@ -146,13 +146,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -5);
thumb.widthProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.setHeight(5);
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
} else {
thumb.relocate(-5, 0);
thumb.setWidth(5);
thumb.heightProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
}
}
@@ -216,32 +216,32 @@ public class DecoratorSkin extends SkinBase<Decorator> {
HBox navLeft = new HBox();
navLeft.setAlignment(Pos.CENTER_LEFT);
navLeft.setPadding(new Insets(0, 5, 0, 5));
{
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
JFXButton closeNavButton = new JFXButton();
JFXButton backNavButton = new JFXButton();
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), -1, -1));
backNavButton.getStyleClass().add("jfx-decorator-button");
backNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
backNavButton.onActionProperty().bind(skinnable.onBackNavButtonActionProperty());
backNavButton.visibleProperty().set(canBack);
JFXButton closeNavButton = new JFXButton();
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (canBack) navLeft.getChildren().add(backNavButton);
if (canClose) navLeft.getChildren().add(closeNavButton);
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
closeNavButton.getStyleClass().add("jfx-decorator-button");
closeNavButton.ripplerFillProperty().bind(Theme.whiteFillBinding());
closeNavButton.onActionProperty().bind(skinnable.onCloseNavButtonActionProperty());
if (canBack) navLeft.getChildren().add(backNavButton);
if (canClose) navLeft.getChildren().add(closeNavButton);
if (showCloseAsHome)
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
else
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), -1, -1));
}
navBar.setLeft(navLeft);
BorderPane center = new BorderPane();
if (title != null) {
Label titleLabel = new Label();
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleBarTransparent) titleLabel.pseudoClassStateChanged(TRANSPARENT, true);
if (!canClose && !canBack) {
@@ -250,6 +250,11 @@ public class DecoratorSkin extends SkinBase<Decorator> {
// we should have width 2 * 10 reduced
titleLabel.setPrefWidth(leftPaneWidth - 20);
}
if (titleNode != null) {
titleLabel.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> {
return leftPaneWidth - 20 - backNavButton.getWidth() - closeNavButton.getWidth();
}, backNavButton.widthProperty(), closeNavButton.widthProperty()));
}
titleLabel.setText(title);
center.setLeft(titleLabel);
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
@@ -47,7 +47,7 @@ public class InstallerWizardDownloadProvider implements DownloadProvider {
@Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) {
return fallback.injectURLWithCandidates(assetObjectLocation);
return fallback.getAssetObjectCandidates(assetObjectLocation);
}
@Override
+17 -9
View File
@@ -367,13 +367,16 @@
.jfx-tool-bar {
-fx-font-size: 13.0;
-fx-font-weight: BOLD;
-fx-pref-width: 100.0%;
-fx-min-height: 40px;
-fx-max-height: 40px;
-fx-pref-height: 40px;
}
.jfx-tool-bar .jfx-decorator-title {
-fx-font-weight: BOLD;
}
.jfx-tool-bar .background {
-fx-background-color: -fx-base-color;
}
@@ -948,37 +951,41 @@
-fx-text-fill: gray;
}
.log {
.log-window .scroll-bar .thumb {
-fx-fill: rgba(0, 0, 0, 0.5);
}
.log-window .list-cell {
-fx-text-fill: black;
-fx-border-width: 0 0 1 0;
-fx-border-color: #dddddd;
}
.log:empty {
.log-window .list-cell:empty {
-fx-border-width: 0;
}
.log:fatal {
.log-window .list-cell:fatal {
-fx-background-color: #F7A699;
}
.log:error {
.log-window .list-cell:error {
-fx-background-color: #FFCCBB;
}
.log:warn {
.log-window .list-cell:warn {
-fx-background-color: #FFEECC;
}
.log:info {
.log-window .list-cell:info {
-fx-background-color: #FFFFFF;
}
.log:debug {
.log-window .list-cell:debug {
-fx-background-color: #EEE9E0;
}
.log:trace {
.log-window .list-cell:trace {
-fx-background-color: #EEE9E0;
}
@@ -1131,6 +1138,7 @@
.jfx-decorator-tab .tab-container:selected .tab-label {
-fx-text-fill: -fx-base-text-fill;
-fx-font-weight: BOLD;
}
.window {
@@ -168,13 +168,13 @@ lang.default=Use system language
launch.advice.corrected=We have already fixed the JVM selection. If you want to keep your choice of Java version, you can disable the Java VM check in game settings.
launch.advice.uncorrected=If you are sure that the game can be started normally, you can disable the Java VM check in game settings.
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.forge2760_liteloader=Forge 2760 and higher are not compatible with LiteLoader, please consider upgrading Forge to 2773 or later.
launch.advice.forge2760_liteloader=Forge 2760 and higher are not compatible with LiteLoader, please consider upgrading Forge to 2773 or later. Shall we continue launching?
launch.advice.java8_1_13=Minecraft 1.13 and later can only run on Java 8 or later.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. Please install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later versions of Java.
launch.advice.newer_java=Java 8 is recommended to make the game faster. For many Minecraft 1.12 of higher, and most mods, Java 8 is required.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. The launcher attempt to launch.
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. The launcher will attempt to launch.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. Shall we continue launching?
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. Shall we continue launching?
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
launch.failed.creating_process=Failed to create process. Check your Java path.
@@ -167,13 +167,13 @@ lang.default=使用系統語言
launch.advice.corrected=我們已經修正了問題。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者變更 Forge 至 2773 或更新的版本。
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者變更 Forge 至 2773 或更新的版本。是否繼續啟動?
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod 的 Minecraft 版本不支援 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=偵測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更順暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要 Java 8 版本。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。啟動器仍會嘗試啟動。
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。啟動器仍會嘗試啟動。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。是否繼續啟動?
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。是否繼續啟動?
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新處理程序時發生錯誤。可能是 Java 路徑錯誤。
@@ -167,13 +167,13 @@ lang.default=跟随系统语言
launch.advice.corrected=我们已经修复了问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。是否继续启动?
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏。是否继续启动?
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。是否继续启动?
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
@@ -63,8 +63,20 @@ public class AdaptedDownloadProvider implements DownloadProvider {
public List<URL> injectURLWithCandidates(String baseURL) {
List<DownloadProvider> d = downloadProviderCandidates;
List<URL> results = new ArrayList<>(d.size());
for (int i = 0; i < d.size(); i++) {
results.add(NetworkUtils.toURL(d.get(i).injectURL(baseURL)));
for (DownloadProvider downloadProvider : d) {
results.add(NetworkUtils.toURL(downloadProvider.injectURL(baseURL)));
}
return results;
}
@Override
public List<URL> injectURLsWithCandidates(List<String> urls) {
List<DownloadProvider> d = downloadProviderCandidates;
List<URL> results = new ArrayList<>(d.size());
for (DownloadProvider downloadProvider : d) {
for (String baseURL : urls) {
results.add(NetworkUtils.toURL(downloadProvider.injectURL(baseURL)));
}
}
return results;
}
@@ -88,8 +88,8 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
+ (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : "");
String fileName1 = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat();
String fileName2 = "forge-" + classifier + "-" + gameVersion + "-" + file.getCategory() + "." + file.getFormat();
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + gameVersion + "/" + fileName2);
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1);
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + gameVersion + "/" + fileName2);
urls.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf(
pair("mcversion", version.getGameVersion()),
pair("version", version.getVersion()),
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -27,6 +28,7 @@ import org.jackhuang.hmcl.task.Task;
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.versioning.VersionNumber;
import java.io.IOException;
import java.nio.file.FileSystem;
@@ -97,7 +99,19 @@ public final class ForgeInstallTask extends Task<Version> {
}
@Override
public void execute() throws IOException, VersionMismatchException {
public void execute() throws IOException, VersionMismatchException, OptiFineInstallTask.UnsupportedOptiFineInstallationException {
String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass();
if (VersionNumber.VERSION_COMPARATOR.compare("1.13", remote.getGameVersion()) <= 0) {
// Forge 1.13 is not compatible with any other libraries.
if (!"net.minecraft.client.main.Main".equals(originalMainClass) && !"cpw.mods.modlauncher.Launcher".equals(originalMainClass))
throw new OptiFineInstallTask.UnsupportedOptiFineInstallationException();
} else {
// Forge 1.12 and older versions is compatible with vanilla and launchwrapper.
// if (!"net.minecraft.client.main.Main".equals(originalMainClass) && !"net.minecraft.launchwrapper.Launch".equals(originalMainClass))
// throw new OptiFineInstallTask.UnsupportedOptiFineInstallationException();
}
if (detectForgeInstallerType(dependencyManager, version, installer))
dependency = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer);
else
@@ -131,10 +131,6 @@ public class ForgeNewInstallTask extends Task<Version> {
@Override
public void execute() throws Exception {
String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass();
if (!"net.minecraft.client.main.Main".equals(originalMainClass) && !"cpw.mods.modlauncher.Launcher".equals(originalMainClass))
throw new OptiFineInstallTask.UnsupportedOptiFineInstallationException();
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
@@ -22,12 +22,14 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.logging.Level;
/**
@@ -84,7 +86,18 @@ public final class GameLibrariesTask extends Task<Void> {
if (!download && integrityCheck && !library.getDownload().validateChecksum(jar, true)) download = true;
if (!download && integrityCheck &&
library.getChecksums() != null && !library.getChecksums().isEmpty() &&
!LibraryDownloadTask.checksumValid(jar.toFile(), library.getChecksums())) download = true;
!LibraryDownloadTask.checksumValid(file, library.getChecksums())) download = true;
if (!download && integrityCheck) {
String ext = FileUtils.getExtension(file);
if (ext.equals("jar")) {
try (JarFile jarFile = new JarFile(file)) {
jarFile.getManifest();
} catch (IOException ignored) {
// the Jar file is malformed, so re-download it.
download = true;
}
}
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + jar, e);
}
@@ -26,6 +26,8 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -34,13 +36,11 @@ import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.jar.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
@@ -30,13 +30,16 @@ import org.jackhuang.hmcl.event.RemoveVersionEvent;
import org.jackhuang.hmcl.event.RenameVersionEvent;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -154,28 +157,32 @@ public class DefaultGameRepository implements GameRepository {
try {
Version fromVersion = getVersion(from);
File fromDir = getVersionRoot(from);
File toDir = getVersionRoot(to);
if (!fromDir.renameTo(toDir))
return false;
Path fromDir = getVersionRoot(from).toPath();
Path toDir = getVersionRoot(to).toPath();
Files.move(fromDir, toDir);
File toJson = new File(toDir, to + ".json");
File toJar = new File(toDir, to + ".jar");
Path fromJson = toDir.resolve(from + ".json");
Path fromJar = toDir.resolve(from + ".jar");
Path toJson = toDir.resolve(to + ".json");
Path toJar = toDir.resolve(to + ".jar");
if (!new File(toDir, from + ".json").renameTo(toJson)
|| !new File(toDir, from + ".jar").renameTo(toJar)) {
try {
Files.move(fromJson, toJson);
Files.move(fromJar, toJar);
} catch (IOException e) {
// recovery
toJson.renameTo(new File(toDir, from + ".json"));
toJar.renameTo(new File(toDir, from + ".jar"));
toDir.renameTo(fromDir);
return false;
Lang.ignoringException(() -> Files.move(toJson, fromJson));
Lang.ignoringException(() -> Files.move(toJar, fromJar));
Lang.ignoringException(() -> Files.move(toDir, fromDir));
throw e;
}
if (fromVersion.getId().equals(fromVersion.getJar()))
fromVersion = fromVersion.setJar(null);
FileUtils.writeText(toJson, JsonUtils.GSON.toJson(fromVersion.setId(to)));
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(to)));
return true;
} catch (IOException | JsonParseException | VersionNotFoundException e) {
LOG.log(Level.WARNING, "Unable to rename version " + from + " to " + to, e);
return false;
}
}
@@ -292,7 +292,7 @@ public class DefaultLauncher extends Launcher {
.replace("$INST_MC_DIR", repository.getRunDirectory(version.getId()).getAbsolutePath())
.replace("$INST_JAVA", options.getJava().getBinary().toString());
new ProcessBuilder(preLaunchCommand)
new ProcessBuilder(StringUtils.tokenize(preLaunchCommand))
.directory(runDirectory).start().waitFor();
}
@@ -31,6 +31,7 @@ import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
@@ -91,6 +92,7 @@ public class FileDownloadTask extends Task<Void> {
private CacheRepository repository = CacheRepository.getInstance();
private RandomAccessFile rFile;
private InputStream stream;
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
/**
* @param url the URL of remote file.
@@ -154,6 +156,8 @@ public class FileDownloadTask extends Task<Void> {
this.integrityCheck = integrityCheck;
this.retry = retry;
this.addIntegrityCheckHandler(ZIP_INTEGRITY_CHECK_HANDLER);
setName(file.getName());
setExecutor(Schedulers.io());
}
@@ -196,6 +200,10 @@ public class FileDownloadTask extends Task<Void> {
return this;
}
public void addIntegrityCheckHandler(IntegrityCheckHandler handler) {
integrityCheckHandlers.add(Objects.requireNonNull(handler));
}
@Override
public void execute() throws Exception {
boolean checkETag;
@@ -219,88 +227,99 @@ public class FileDownloadTask extends Task<Void> {
Exception exception = null;
URL failedURL = null;
for (int repeat = 0; repeat < retry * urls.size(); repeat++) {
URL url = urls.get(repeat / retry);
if (isCancelled()) {
break;
}
Logging.LOG.log(Level.FINER, "Downloading " + url + " to " + file);
Path temp = null;
try {
updateProgress(0);
HttpURLConnection con = NetworkUtils.createConnection(url);
if (checkETag) repository.injectConnection(con);
con = NetworkUtils.resolveConnection(con);
if (con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache
try {
Path cache = repository.getCachedRemoteFile(con);
FileUtils.copyFile(cache.toFile(), file);
return;
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
repository.removeRemoteEntry(con);
// Now we must reconnect the server since 304 may result in empty content,
// if we want to redownload the file, we must reconnect the server without etag settings.
repeat--;
continue;
}
} else if (con.getResponseCode() / 100 != 2) {
throw new ResponseCodeException(url, con.getResponseCode());
int repeat = 0;
download: for (URL url : urls) {
for (int retryTime = 0; retryTime < retry; retryTime++) {
if (isCancelled()) {
break download;
}
int contentLength = con.getContentLength();
if (contentLength < 0)
throw new IOException("The content length is invalid.");
Logging.LOG.log(Level.FINER, "Downloading " + url + " to " + file);
Path temp = null;
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Could not make directory " + file.getAbsoluteFile().getParent());
try {
updateProgress(0);
temp = Files.createTempFile(null, null);
rFile = new RandomAccessFile(temp.toFile(), "rw");
HttpURLConnection con = NetworkUtils.createConnection(url);
if (checkETag) repository.injectConnection(con);
con = NetworkUtils.resolveConnection(con);
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
while (true) {
if (isCancelled()) {
break;
if (con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache
try {
Path cache = repository.getCachedRemoteFile(con);
FileUtils.copyFile(cache.toFile(), file);
return;
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to use cached file, redownload it", e);
repository.removeRemoteEntry(con);
// Now we must reconnect the server since 304 may result in empty content,
// if we want to redownload the file, we must reconnect the server without etag settings.
retryTime--;
continue;
}
} else if (con.getResponseCode() / 100 == 4) {
break; // we will not try this URL again
} else if (con.getResponseCode() / 100 != 2) {
throw new ResponseCodeException(url, con.getResponseCode());
}
int read = stream.read(buffer);
if (read == -1)
break;
int contentLength = con.getContentLength();
if (contentLength < 0)
throw new IOException("The content length is invalid.");
if (digest != null) {
digest.update(buffer, 0, read);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Could not make directory " + file.getAbsoluteFile().getParent());
temp = Files.createTempFile(null, null);
rFile = new RandomAccessFile(temp.toFile(), "rw");
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
while (true) {
if (isCancelled()) {
break;
}
int read = stream.read(buffer);
if (read == -1)
break;
if (digest != null) {
digest.update(buffer, 0, read);
}
// Write buffer to file.
rFile.write(buffer, 0, read);
downloaded += read;
// Update progress information per second
updateProgress(downloaded, contentLength);
updateDownloadSpeed(downloaded - lastDownloaded);
lastDownloaded = downloaded;
}
// Write buffer to file.
rFile.write(buffer, 0, read);
downloaded += read;
// Update progress information per second
updateProgress(downloaded, contentLength);
updateDownloadSpeed(downloaded - lastDownloaded);
lastDownloaded = downloaded;
}
updateDownloadSpeed(downloaded - lastDownloaded);
closeFiles();
closeFiles();
if (downloaded != contentLength)
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
// Restore temp file to original name.
if (isCancelled()) {
temp.toFile().delete();
break download;
}
for (IntegrityCheckHandler handler : integrityCheckHandlers) {
handler.checkIntegrity(temp, file.toPath());
}
// Restore temp file to original name.
if (isCancelled()) {
temp.toFile().delete();
break;
} else {
Files.deleteIfExists(file.toPath());
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Unable to make parent directory " + file);
@@ -309,37 +328,34 @@ public class FileDownloadTask extends Task<Void> {
} catch (Exception e) {
throw new IOException("Unable to move temp file from " + temp + " to " + file, e);
}
}
if (downloaded != contentLength)
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
// Integrity check
if (integrityCheck != null) {
integrityCheck.performCheck(digest);
}
if (caching && integrityCheck != null) {
try {
repository.cacheFile(file.toPath(), integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to cache file", e);
// Integrity check
if (integrityCheck != null) {
integrityCheck.performCheck(digest);
}
}
if (checkETag) {
repository.cacheRemoteFile(file.toPath(), con);
}
if (caching && integrityCheck != null) {
try {
repository.cacheFile(file.toPath(), integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to cache file", e);
}
}
return;
} catch (IOException e) {
if (temp != null)
temp.toFile().delete();
failedURL = url;
exception = e;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (repeat + 1), e);
} finally {
closeFiles();
if (checkETag) {
repository.cacheRemoteFile(file.toPath(), con);
}
return;
} catch (IOException e) {
if (temp != null)
temp.toFile().delete();
failedURL = url;
exception = e;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat), e);
} finally {
closeFiles();
}
}
}
@@ -375,7 +391,7 @@ public class FileDownloadTask extends Task<Void> {
/**
* Download speed in byte/sec.
* @return
* @return download speed
*/
public int getSpeed() {
return speed;
@@ -387,4 +403,22 @@ public class FileDownloadTask extends Task<Void> {
}
}
public interface IntegrityCheckHandler {
/**
* Check whether the file is corrupted or not.
* @param filePath the file locates in (maybe in temp directory)
* @param destinationPath for real file name
* @throws IOException if the file is corrupted
*/
void checkIntegrity(Path filePath, Path destinationPath) throws IOException;
}
public static final IntegrityCheckHandler ZIP_INTEGRITY_CHECK_HANDLER = (filePath, destinationPath) -> {
String ext = FileUtils.getExtension(destinationPath).toLowerCase();
if (ext.equals("zip") || ext.equals("jar")) {
try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) {
// test for zip format
}
}
};
}