Compare commits

..
28 Commits
Author SHA1 Message Date
huanghongxun a141c6b137 Display changelog when update 2019-05-09 01:51:51 +08:00
huanghongxun 300efb99d2 Fix #540 2019-05-09 00:49:10 +08:00
yushijinhun 6fb3610bcc Refresh skins on startup
Mentioned in #539
2019-05-02 12:40:46 +08:00
yushijinhun 43fb3ca10a Fix illegal texturePayload may cause crash 2019-05-02 12:35:43 +08:00
yushijinhun adc360eaaa Fix #548 2019-05-02 00:12:33 +08:00
huanghongxun 5e659352d7 Support install Forge/OptiFine from local file 2019-04-30 20:26:31 +08:00
huanghongxun 6595e0a3cf Remove authlib-injector-servers.fxml 2019-04-30 00:29:19 +08:00
huanghongxun c43b2b3bb9 Fix potential "not on UI thread" exception 2019-04-30 00:28:22 +08:00
huanghongxun ea19e6efd8 Add Fabric installerItem in list page 2019-04-29 17:57:04 +08:00
huanghongxun 55d38f2b36 Fix importing fabric multimc instance 2019-04-28 14:18:04 +08:00
huanghongxun e0abf3e0c1 Support fabric detection 2019-04-28 14:17:28 +08:00
huanghongxun 974cba8dee Suppress NPE in LogWindow 2019-04-25 13:56:44 +08:00
huanghongxun 019cf5fc8f Suppress NPE 2019-04-25 13:50:51 +08:00
huanghongxun 1956ba7a73 Force refreshing version list when remove version from disk 2019-04-25 13:39:07 +08:00
huanghongxun c209617ee7 Try to fix ConcurrentModificationException 2019-04-25 13:38:44 +08:00
huanghongxun f4b5e061a4 Suppress NPE 2019-04-25 13:38:12 +08:00
huanghongxun 06b67a4b9a Suppress image resource not found 2019-04-25 12:56:04 +08:00
huanghongxun a385265956 Fix not on ui thread 2019-04-25 11:31:01 +08:00
huanghongxun 6ac32a40c7 Suppress UncheckedIOException thrown by loading alex and steve skin image. 2019-04-25 11:28:49 +08:00
huanghongxun 20c612c660 Fix #537 2019-04-25 11:20:58 +08:00
huanghongxun 7e8cc5fe6c Fix #547 2019-04-25 11:16:59 +08:00
huanghongxun 9ed14b227b Workaround fix for NPE when remove mods 2019-04-25 10:49:25 +08:00
huanghongxun ee466d0f4c Fix #545 2019-04-03 17:02:08 +08:00
huanghongxun b198eea9e4 Friendly prompt if response code is not OK 2019-04-03 14:05:14 +08:00
huanghongxun 8c7580488e More friendly prompt for SocketTimeoutException 2019-03-24 14:30:58 +08:00
huanghongxun 3f4364c67c Fix cannot launch a game multiple times 2019-03-24 12:14:13 +08:00
huanghongxun 23e2df5e58 Unzip natives to /versions/ reducing trashes in temp 2019-03-21 11:08:00 +08:00
huanghongxun 79136db162 Lower XZ preset 2019-03-19 11:11:59 +08:00
77 changed files with 1082 additions and 418 deletions

No files matched your search

+1 -1
View File
@@ -140,7 +140,7 @@ task makePack(dependsOn: jar) {
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(6)).withCloseable { it << makePack.outputPath.bytes }
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
@@ -35,6 +35,7 @@ public final class Metadata {
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
@@ -31,6 +31,8 @@ import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
@@ -150,18 +152,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
Version version = getVersion(id);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| "cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return newImage("/assets/img/furnace.png");
else
return new Image("/assets/img/grass.png");
return newImage("/assets/img/grass.png");
}
public boolean saveVersionSetting(String id) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
@@ -398,7 +399,10 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(Controllers.getStage()::show);
Platform.runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
break;
case KEEP:
// No operations here
@@ -17,13 +17,21 @@
*/
package org.jackhuang.hmcl.game;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.awt.Graphics2D;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
@@ -40,24 +48,11 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* @author yushijinhun
@@ -140,8 +135,9 @@ public final class TexturesLoader {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
}
private static void loadDefaultSkin(String path, TextureModel model) {
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -25,6 +25,7 @@ import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -81,7 +82,7 @@ public class Theme {
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
@@ -60,6 +59,7 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -213,7 +213,7 @@ public final class Controllers {
scene = new Scene(decorator.getDecorator(), 800, 519);
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
}
@@ -22,7 +22,6 @@ import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
@@ -30,6 +29,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -67,7 +67,7 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
@@ -34,6 +34,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
@@ -45,6 +46,7 @@ import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
@@ -484,6 +486,21 @@ public final class FXUtils {
}
}
/**
* Suppress IllegalArgumentException since the url is supposed to be correct definitely.
* @param url the url of image. The image resource should be a file within the jar.
* @return the image resource within the jar.
* @see org.jackhuang.hmcl.util.CrashReporter
* @see ResourceNotFoundError
*/
public static Image newImage(String url) {
try {
return new Image(url);
} catch (IllegalArgumentException e) {
throw new ResourceNotFoundError("Cannot access image: " + url, e);
}
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -24,6 +24,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
@@ -34,7 +35,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public class InstallerItem extends BorderPane {
public InstallerItem(String artifact, String version, Runnable upgrade, Consumer<InstallerItem> deleteCallback) {
public InstallerItem(String artifact, String version, @Nullable Runnable upgrade, @Nullable Consumer<InstallerItem> deleteCallback) {
getStyleClass().add("two-line-list-item");
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
@@ -49,19 +50,24 @@ public class InstallerItem extends BorderPane {
{
HBox hBox = new HBox();
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
upgradeButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
upgradeButton.setOnMouseClicked(e -> upgrade.run());
if (upgrade != null) {
JFXButton upgradeButton = new JFXButton();
upgradeButton.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
upgradeButton.getStyleClass().add("toggle-icon4");
FXUtils.installFastTooltip(upgradeButton, i18n("install.change_version"));
upgradeButton.setOnMouseClicked(e -> upgrade.run());
hBox.getChildren().add(upgradeButton);
}
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
if (deleteCallback != null) {
JFXButton deleteButton = new JFXButton();
deleteButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
deleteButton.getStyleClass().add("toggle-icon4");
deleteButton.setOnMouseClicked(e -> deleteCallback.accept(this));
hBox.getChildren().add(deleteButton);
}
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.getChildren().setAll(upgradeButton, deleteButton);
setRight(hBox);
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import javafx.application.Platform;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -42,6 +41,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -70,12 +70,12 @@ public final class LeftPaneController extends AdvancedListBox {
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.setImage(new Image("/assets/img/bookshelf.png"));
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage()));
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.setImage(new Image("/assets/img/command.png"));
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> Controllers.navigate(Controllers.getSettingsPage()));
@@ -120,11 +120,10 @@ public final class LeftPaneController extends AdvancedListBox {
if (modpackFile.exists()) {
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
.thenApply(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
.thenAccept(modpack -> {
AtomicReference<Region> region = new AtomicReference<>();
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor();
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
.thenApply(modpack -> ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor())
.thenAccept(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"));
executor.start();
}).start();
}
@@ -39,6 +39,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("large-spinner-pane");
Pane placeholder = new Pane();
VBox list = new VBox();
StackPane contentPane = new StackPane();
{
@@ -46,10 +47,8 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
{
scrollPane.setFitToWidth(true);
VBox list = new VBox();
list.maxWidthProperty().bind(scrollPane.widthProperty());
list.setSpacing(10);
list.setPadding(new Insets(10));
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
@@ -87,8 +86,13 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
FXUtils.onChangeAndOperate(skinnable.refreshableProperty(),
refreshable -> {
if (refreshable) vBox.getChildren().setAll(btnRefresh, btnAdd);
else vBox.getChildren().setAll(btnAdd);
if (refreshable) {
list.setPadding(new Insets(10, 10, 15 + 40 + 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnRefresh, btnAdd);
} else {
list.setPadding(new Insets(10, 10, 15 + 40 + 15, 10));
vBox.getChildren().setAll(btnAdd);
}
});
}
@@ -25,7 +25,6 @@ import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
@@ -35,6 +34,7 @@ import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.w3c.dom.Document;
@@ -44,6 +44,7 @@ import org.w3c.dom.Node;
import java.util.concurrent.CountDownLatch;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -65,7 +66,7 @@ public final class LogWindow extends Stage {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
}
public LogWindow(String text) {
@@ -174,7 +175,7 @@ public final class LogWindow extends Stage {
FXUtils.loadFXML(this, "/assets/fxml/log.fxml");
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", config().getFontSize() + "px \"" + config().getFontFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
@@ -0,0 +1,82 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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/>.
*/
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
private final WebView webView = new WebView();
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
{
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String url = engine.getLoadWorker().getMessage().trim();
if (!url.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(url);
}
});
setBody(webView);
}
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
setActions(updateButton, cancelButton);
}
}
}
@@ -18,11 +18,11 @@
package org.jackhuang.hmcl.ui;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class WebStage extends Stage {
private final WebView webView = new WebView();
@@ -30,7 +30,7 @@ public class WebStage extends Stage {
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
getIcons().add(newImage("/assets/img/icon.png"));
}
public WebView getWebView() {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
@@ -29,6 +28,7 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
@@ -42,7 +42,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(new Image("/assets/img/craft_table.png"));
setImage(newImage("/assets/img/craft_table.png"));
} else {
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account));
@@ -100,7 +100,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -27,6 +27,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -35,21 +36,14 @@ import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerItem> implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("account.injector.manage.title"));
@FXML private ScrollPane scrollPane;
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private ObservableList<AuthlibInjectorServerItem> serverItems;
private final ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
Bindings.bindContent(itemsProperty(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
@@ -57,8 +51,8 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
@Override
public void add() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
@@ -71,6 +71,7 @@ public class RipplerContainer extends StackPane {
setContainer(container);
getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
buttonContainer.getChildren().add(buttonRippler);
focusedProperty().addListener((a, b, newValue) -> {
if (newValue) {
@@ -45,7 +45,10 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
@@ -61,6 +64,7 @@ import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class DecoratorController {
@@ -92,7 +96,7 @@ public class DecoratorController {
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(new Image("/assets/img/welcome.png"));
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
@@ -152,7 +156,7 @@ public class DecoratorController {
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
private Image defaultBackground = newImage("/assets/img/background.jpg");
/**
* Load background image from bg/, background.png, background.jpg
@@ -21,6 +21,7 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Library;
@@ -34,9 +35,13 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import java.net.SocketTimeoutException;
import java.util.Map;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class InstallerWizardProvider implements WizardProvider {
@@ -53,9 +58,9 @@ public final class InstallerWizardProvider implements WizardProvider {
this.version = version;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
forge = analyzer.getForge().map(Library::getVersion).orElse(null);
liteLoader = analyzer.getLiteLoader().map(Library::getVersion).orElse(null);
optiFine = analyzer.getOptiFine().map(Library::getVersion).orElse(null);
forge = analyzer.get(FORGE).map(Library::getVersion).orElse(null);
liteLoader = analyzer.get(LITELOADER).map(Library::getVersion).orElse(null);
optiFine = analyzer.get(OPTIFINE).map(Library::getVersion).orElse(null);
}
public Profile getProfile() {
@@ -125,9 +130,25 @@ public final class InstallerWizardProvider implements WizardProvider {
if (exception instanceof LibraryDownloadException) {
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode()) + ", " + ((DownloadException) exception).getUrl() + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
}
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof UnsupportedOperationException) {
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof VersionMismatchException) {
VersionMismatchException e = ((VersionMismatchException) exception);
Controllers.dialog(i18n("install.failed.version_mismatch", e.getExpect(), e.getActual()), i18n("install.failed"), MessageType.ERROR, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageType.ERROR, next);
}
@@ -85,4 +85,11 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
public boolean cancel() {
return true;
}
@Override
public boolean cancelIfCannotGoBack() {
// VersionsPage will call wizardController.onPrev(cleanUp = true) when list is empty.
// So we cancel this wizard when VersionPage calls the method.
return true;
}
}
@@ -73,9 +73,9 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version))
if (file.isFile() && Objects.equals(FileUtils.getNameWithoutExtension(file), version)) // Ignore <version>.json, <version>.jar
state = ModAdviser.ModSuggestion.HIDDEN;
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives"))
if (file.isDirectory() && Objects.equals(file.getName(), version + "-natives")) // Ignore <version>-natives
state = ModAdviser.ModSuggestion.HIDDEN;
if (state == ModAdviser.ModSuggestion.HIDDEN)
return null;
@@ -19,13 +19,14 @@ package org.jackhuang.hmcl.ui.profile;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class ProfileAdvancedListItem extends AdvancedListItem {
private ObjectProperty<Profile> profile = new SimpleObjectProperty<Profile>() {
@@ -41,7 +42,7 @@ public class ProfileAdvancedListItem extends AdvancedListItem {
};
public ProfileAdvancedListItem() {
setImage(new Image("/assets/img/craft_table.png"));
setImage(newImage("/assets/img/craft_table.png"));
setRightGraphic(SVG.viewList(Theme.blackFillBinding(), -1, -1));
}
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
@@ -31,6 +30,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
public ProfileListItemSkin(ProfileListItem skinnable) {
@@ -54,7 +55,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().set(new Image("/assets/img/craft_table.png"));
imageView.imageProperty().set(newImage("/assets/img/craft_table.png"));
TwoLineListItem item = new TwoLineListItem();
BorderPane.setAlignment(item, Pos.CENTER);
@@ -72,7 +73,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
@@ -24,6 +24,7 @@ import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -80,7 +81,7 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
public void refresh() {
setLoading(true);
Task.of(datapack::loadFromDir)
.with(Task.of(() -> setLoading(false)))
.with(Task.of(Schedulers.javafx(), () -> setLoading(false)))
.start();
}
@@ -17,13 +17,13 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
@@ -38,7 +38,7 @@ public class GameAdvancedListItem extends AdvancedListItem {
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
setImage(new Image("/assets/img/grass.png"));
setImage(newImage("/assets/img/grass.png"));
}
});
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.setting.Profile;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
@@ -55,9 +56,9 @@ public class GameItem extends Control {
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getVersion(id));
analyzer.getForge().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.getOptiFine().ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
analyzer.get(FORGE).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))));
analyzer.get(LITELOADER).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))));
analyzer.get(OPTIFINE).ifPresent(library -> libraries.append(", ").append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))));
subtitle.set(libraries.toString());
}, Platform::runLater)
.exceptionally(handleUncaught);
@@ -87,7 +87,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
right.getChildren().add(btnManage);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 12;");
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
@@ -26,24 +29,42 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallerListPage extends ListPage<InstallerItem> {
public class InstallerListPage extends ListPageBase<InstallerItem> {
private Profile profile;
private String versionId;
private Version version;
private String gameVersion;
{
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "exe").contains(FileUtils.getExtension(it)), mods -> {
if (!mods.isEmpty())
doInstallOffline(mods.get(0));
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new InstallerListPageSkin();
}
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
@@ -66,26 +87,70 @@ public class InstallerListPage extends ListPage<InstallerItem> {
};
itemsProperty().clear();
analyzer.getForge().ifPresent(library -> itemsProperty().add(
analyzer.get(FORGE).ifPresent(library -> itemsProperty().add(
new InstallerItem("Forge", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "forge", library));
}, removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> itemsProperty().add(
analyzer.get(LITELOADER).ifPresent(library -> itemsProperty().add(
new InstallerItem("LiteLoader", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "liteloader", library));
}, removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> itemsProperty().add(
analyzer.get(OPTIFINE).ifPresent(library -> itemsProperty().add(
new InstallerItem("OptiFine", library.getVersion(), () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, "optifine", library));
}, removeAction.apply(library))));
analyzer.get(FABRIC).ifPresent(library -> itemsProperty().add(new InstallerItem("Fabric", library.getVersion(), null, null)));
}).start();
}
@Override
public void add() {
public void installOnline() {
if (gameVersion == null)
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion, version));
}
public void installOffline() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("install.installer.install_offline.extension"), "*.jar", "*.exe"));
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null) doInstallOffline(file);
}
private void doInstallOffline(File file) {
Task task = profile.getDependency().installLibraryAsync(version, file.toPath())
.then(profile.getRepository().refreshVersionsAsync());
task.setName(i18n("install.installer.install_offline"));
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
runInFX(() -> {
if (success) {
loadVersion(profile, versionId);
Controllers.dialog(i18n("install.success"));
} else {
if (executor.getLastException() == null)
return;
InstallerWizardProvider.alertFailureMessage(executor.getLastException(), null);
}
});
}
});
Controllers.taskDialog(executor, i18n("install.installer.install_offline"));
executor.start();
}
private class InstallerListPageSkin extends ToolbarListPageSkin<InstallerListPage> {
InstallerListPageSkin() {
super(InstallerListPage.this);
}
@Override
protected List<Node> initializeToolbar(InstallerListPage skinnable) {
return Arrays.asList(
createToolbarButton(i18n("install.installer.install_online"), SVG::plus, skinnable::installOnline),
createToolbarButton(i18n("install.installer.install_offline"), SVG::plus, skinnable::installOffline));
}
}
}
@@ -41,6 +41,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -79,7 +80,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(id));
modded.set(libraryAnalyzer.hasForge() || libraryAnalyzer.hasLiteLoader());
modded.set(libraryAnalyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
@@ -142,6 +143,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
try {
modManager.removeMods(selectedItems.stream()
.map(TreeItem::getValue)
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.toArray(ModInfo[]::new));
loadMods(modManager);
@@ -34,11 +34,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -61,6 +57,7 @@ import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -316,7 +313,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private void loadIcon() {
if (versionId == null) {
iconPickerItem.setImage(new Image("/assets/img/grass.png"));
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
return;
}
@@ -324,7 +321,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(new Image("/assets/img/grass.png"));
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
@@ -50,9 +50,7 @@ public class Versions {
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
Controllers.confirmDialog(message, i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
}
profile.getRepository().removeVersionFromDisk(version);
}, null);
}
@@ -64,7 +64,6 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
public void onStop(boolean success, TaskExecutor executor) {
runInFX(() -> {
pane.fireEvent(new DialogCloseEvent());
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageType.FINE, () -> onEnd());
@@ -83,7 +83,12 @@ public class WizardController implements Navigation {
@Override
public void onPrev(boolean cleanUp) {
if (!canPrev()) {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
if (provider.cancelIfCannotGoBack()) {
onCancel();
return;
} else {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
}
}
Node page = pages.pop();
@@ -26,6 +26,9 @@ public interface WizardProvider {
Object finish(Map<String, Object> settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
boolean cancel();
default boolean cancelIfCannotGoBack() {
return false;
}
interface FailureCallback {
void onFail(Map<String, Object> settings, Exception exception, Runnable next);
@@ -70,6 +70,10 @@ public final class IntegrityChecker {
for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) {
String filename = entry.getName();
try (InputStream in = zip.getInputStream(entry)) {
if (in == null) {
throw new IOException("entry is null");
}
if (SIGNATURE_FILE.equals(filename)) {
signature = IOUtils.readFullyAsByteArray(in);
} else {
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -91,41 +92,42 @@ public final class UpdateHandler {
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
Controllers.dialog(new UpgradeDialog(() -> {
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
Task task = new HMCLDownloadTask(version, downloaded);
Task task = new HMCLDownloadTask(version, downloaded);
TaskExecutor executor = task.executor();
Controllers.taskDialog(executor, i18n("message.downloading"));
thread(() -> {
boolean success = executor.test();
TaskExecutor executor = task.executor();
Controllers.taskDialog(executor, i18n("message.downloading"));
thread(() -> {
boolean success = executor.test();
if (success) {
try {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
if (success) {
try {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(downloaded, getCurrentLocation());
System.exit(0);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
}
requestUpdate(downloaded, getCurrentLocation());
System.exit(0);
} catch (IOException e) {
} else {
Throwable e = executor.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
return;
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
} else {
Throwable e = executor.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
}
});
});
}));
}
private static void applyUpdate(Path target) throws IOException {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
@@ -52,6 +51,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
put("Location is not set", i18n("crash.NoClassDefFound"));
put("UnsatisfiedLinkError", i18n("crash.user_fault"));
put("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound"));
put("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
@@ -0,0 +1,41 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> 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.util;
import java.io.InputStream;
/**
* Suppress the throwable when we make sure the resource cannot miss.
* @see CrashReporter
*/
public class ResourceNotFoundError extends Error {
public ResourceNotFoundError(String message) {
super(message);
}
public ResourceNotFoundError(String message, Throwable cause) {
super(message, cause);
}
public static InputStream getResourceAsStream(String url) {
InputStream stream = ResourceNotFoundError.class.getResourceAsStream(url);
if (stream == null)
throw new ResourceNotFoundError("Resource not found: " + url);
return stream;
}
}
@@ -57,4 +57,12 @@ public final class I18n {
}
}
public static boolean hasKey(String key) {
try {
getResourceBundle().getString(key);
return true;
} catch (MissingResourceException e) {
return false;
}
}
}
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<StackPane fx:id="contentPane">
<ScrollPane fx:id="scrollPane" fitToWidth="true" fitToHeight="true">
<VBox fx:id="listPane" spacing="10" style="-fx-padding: 20 20 70 20;">
</VBox>
</ScrollPane>
<VBox style="-fx-padding: 15;" spacing="15" pickOnBounds="false" alignment="BOTTOM_RIGHT">
<JFXButton prefWidth="40" prefHeight="40" buttonType="RAISED" onMouseClicked="#onAdd" styleClass="jfx-button-raised-round">
<graphic>
<fx:include source="/assets/svg/plus.fxml"/>
</graphic>
</JFXButton>
</VBox>
</StackPane>
</fx:root>
@@ -19,7 +19,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblForge">Install Forge</Label>
<Label fx:id="lblForge"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -31,7 +31,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblLiteLoader">Install LiteLoader</Label>
<Label fx:id="lblLiteLoader"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -43,7 +43,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblOptiFine">Install OptiFine</Label>
<Label fx:id="lblOptiFine"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -19,7 +19,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblForge">Install Forge</Label>
<Label fx:id="lblForge"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -31,7 +31,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblLiteLoader">Install LiteLoader</Label>
<Label fx:id="lblLiteLoader"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -43,7 +43,7 @@
<graphic>
<BorderPane mouseTransparent="true">
<left>
<Label fx:id="lblOptiFine">Install OptiFine</Label>
<Label fx:id="lblOptiFine"/>
</left>
<right>
<fx:include source="/assets/svg/arrow-right.fxml"/>
@@ -91,6 +91,7 @@ crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
download=Download
download.code.404=File does not found in remote server
download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
@@ -129,11 +130,19 @@ install.change_version.confirm=Sure to update %s from verison %s to %s?
install.failed=Failed to install
install.failed.downloading=Failed to install due to some files not downloaded successfully
install.failed.downloading.detail=Failed to download file: %s
install.failed.downloading.timeout=Download timed out: %s
install.failed.install_online=Unable to recognize what you provided installer file is
install.failed.optifine_conflict=OptiFine and Forge are both installed simultaneously on Minecraft 1.13
install.failed.version_mismatch=The library requires game version %s, but actual version is %s.
install.installer.choose=Choose a %s version
install.installer.forge=Forge
install.installer.game=Game
install.installer.install=
install.installer.install=Install %s
install.installer.install_offline=Install/Upgrade from local file
install.installer.install_offline.extension=Forge/OptiFine installer
install.installer.install_offline.tooltip=Support importing Forge/OptiFine installer jar file
install.installer.install_online=Install Online
install.installer.install_online.tooltip=Support Forge, OptiFine, LiteLoader installation.
install.installer.liteloader=LiteLoader
install.installer.not_installed=%s not Installed
install.installer.optifine=OptiFine
@@ -379,6 +388,8 @@ sponsor.bmclapi=To obtain a stable download service, please consider support BMC
sponsor.hmcl=HMCL is a free, open source Minecraft launcher the enables users to easily manage multiple, separate installations of Minecraft. We're using Afdian so that we can continue to pay for our hosting and project development. Click here for more information.
update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Update to development version
update.channel.stable=Update to stable version
update.checking=Checking for updates
@@ -90,6 +90,7 @@ crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.code.404=遠程伺服器不包含需要下載的文件
download.failed=下載失敗
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
@@ -128,11 +129,19 @@ install.change_version.confirm=你確定要將 %s 從 %s 更新到 %s 嗎?
install.failed=安裝失敗
install.failed.downloading=安裝失敗,部分文件未能完成下載
install.failed.downloading.detail=未能下載檔案:%s
install.failed.optifine_conflict=暫不支持 OptiFine 與 Forge 同時安裝於 Minecraft 1.13
install.failed.downloading.timeout=下載超時:%s
install.failed.install_online=無法識別要安裝的軟體
install.failed.optifine_conflict=暫不支持 OptiFine 與 Forge 同時安裝在 Minecraft 1.13 上
install.failed.version_mismatch=該軟體需要的遊戲版本為 %s,但實際的遊戲版本為 %s。
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.install_offline=從本地檔案安裝/升級
install.installer.install_offline.extension=Forge/OptiFine 安裝器
install.installer.install_offline.tooltip=支持導入已經下載好的 Forge/OptiFine 安裝器
install.installer.install_online=在線安裝
install.installer.install_online.tooltip=支持安裝 Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫時不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
@@ -377,6 +386,8 @@ sponsor.bmclapi=請贊助 BMCLAPI 從而獲得穩定高速的下載服務,點
sponsor.hmcl=HMCL 是一個免費、開源的 Minecraft 啟動器,允許玩家方便快捷地安裝、管理、運行遊戲。您的贊助將幫助 HMCL 獲得更好的發展、支持穩定高速的遊戲安裝與文件下載服務。點擊此處查閱更多詳細訊息。
update=啟動器更新
update.accept=更新
update.changelog=更新日誌
update.channel.dev=更新到開發版
update.channel.stable=更新到推薦版本
update.checking=正在檢查更新
@@ -90,6 +90,7 @@ crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
download.code.404=远程服务器不包含需要下载的文件
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
@@ -128,11 +129,19 @@ install.change_version.confirm=你确定要将 %s 从 %s 更新到 %s 吗?
install.failed=安装失败
install.failed.downloading=安装失败,部分文件未能完成下载
install.failed.downloading.detail=未能下载文件:%s
install.failed.optifine_conflict=暂不支持 OptiFine 与 Forge 同时安装于 Minecraft 1.13
install.failed.downloading.timeout=下载超时:%s
install.failed.install_online=无法识别要安装的软件
install.failed.optifine_conflict=暂不支持 OptiFine 与 Forge 同时安装在 Minecraft 1.13 上
install.failed.version_mismatch=该软件需要的游戏版本为 %s,但实际的游戏版本为 %s。
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
install.installer.install=安装 %s
install.installer.install_offline=从本地文件安装/升级
install.installer.install_offline.extension=Forge/OptiFine 安装器
install.installer.install_offline.tooltip=支持导入已经下载好的 Forge/OptiFine 安装器
install.installer.install_online=在线安装
install.installer.install_online.tooltip=支持安装 Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暂不安装 %s,可以点击此处安装
install.installer.optifine=OptiFine
@@ -377,6 +386,8 @@ sponsor.bmclapi=请赞助 BMCLAPI 从而获得稳定高速的下载服务,点
sponsor.hmcl=HMCL 是一个免费、开源的 Minecraft 启动器,允许玩家方便快捷地安装、管理、运行游戏。您的赞助将帮助 HMCL 获得更好的发展、支持稳定高速的游戏安装与文件下载服务。点击此处查阅更多详细信息。
update=启动器更新
update.accept=更新
update.changelog=更新日志
update.channel.dev=更新到开发版
update.channel.stable=更新到推荐版本
update.checking=正在检查更新
@@ -23,8 +23,11 @@ import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -70,7 +73,9 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository = server.getYggdrasilService().getProfileRepository();
profileRepository.put(selected.getId(), new CompleteGameProfile(selected, properties));
profileRepository.invalidate(selected.getId());
});
return new AuthlibInjectorAccount(server, downloader, username, session);
@@ -34,6 +34,8 @@ import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import javafx.beans.binding.ObjectBinding;
public class YggdrasilAccount extends Account {
private final YggdrasilService service;
@@ -48,6 +50,8 @@ public class YggdrasilAccount extends Account {
this.username = requireNonNull(username);
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
this.session = requireNonNull(session);
addProfilePropertiesListener();
}
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
@@ -73,6 +77,17 @@ public class YggdrasilAccount extends Account {
characterUUID = session.getSelectedProfile().getId();
authenticated = true;
addProfilePropertiesListener();
}
private ObjectBinding<Optional<CompleteGameProfile>> profilePropertiesBinding;
private void addProfilePropertiesListener() {
// binding() is thread-safe
// hold the binding so that it won't be garbage-collected
profilePropertiesBinding = service.getProfileRepository().binding(characterUUID, true);
// and it's safe to add a listener to an ObjectBinding which does not have any listener attached before (maybe tricky)
profilePropertiesBinding.addListener((a, b, c) -> this.invalidate());
}
@Override
@@ -20,9 +20,11 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -63,7 +65,9 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
@SuppressWarnings("unchecked")
Map<String, String> properties = it;
GameProfile selected = session.getSelectedProfile();
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository = service.getProfileRepository();
profileRepository.put(selected.getId(), new CompleteGameProfile(selected, properties));
profileRepository.invalidate(selected.getId());
});
return new YggdrasilAccount(service, username, session);
@@ -165,7 +165,13 @@ public class YggdrasilService {
String encodedTextures = profile.getProperties().get("textures");
if (encodedTextures != null) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
byte[] decodedBinary;
try {
decodedBinary = Base64.getDecoder().decode(encodedTextures);
} catch (IllegalArgumentException e) {
throw new ServerResponseMalformedException(e);
}
TextureResponse texturePayload = fromJson(new String(decodedBinary, UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures);
} else {
return Optional.empty();
@@ -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));
}
}
@@ -20,59 +20,68 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
public final class LibraryAnalyzer {
private final Library forge;
private final Library liteLoader;
private final Library optiFine;
private final Map<LibraryType, Library> libraries;
public LibraryAnalyzer(Library forge, Library liteLoader, Library optiFine) {
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
private LibraryAnalyzer(Map<LibraryType, Library> libraries) {
this.libraries = libraries;
}
public Optional<Library> getForge() {
return Optional.ofNullable(forge);
public Optional<Library> get(LibraryType type) {
return Optional.ofNullable(libraries.get(type));
}
public boolean hasForge() {
return forge != null;
public boolean has(LibraryType type) {
return libraries.containsKey(type);
}
public Optional<Library> getLiteLoader() {
return Optional.ofNullable(liteLoader);
}
public boolean hasLiteLoader() {
return liteLoader != null;
}
public Optional<Library> getOptiFine() {
return Optional.ofNullable(optiFine);
}
public boolean hasOptiFine() {
return optiFine != null;
public boolean hasModLoader() {
return Arrays.stream(LibraryType.values())
.filter(LibraryType::isModLoader)
.anyMatch(this::has);
}
public static LibraryAnalyzer analyze(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
Map<LibraryType, Library> libraries = new EnumMap<>(LibraryType.class);
for (Library library : version.getLibraries()) {
String groupId = library.getGroupId();
String artifactId = library.getArtifactId();
if (groupId.equalsIgnoreCase("net.minecraftforge") && artifactId.equalsIgnoreCase("forge"))
forge = library;
if (groupId.equalsIgnoreCase("com.mumfrey") && artifactId.equalsIgnoreCase("liteloader"))
liteLoader = library;
if ((groupId.equalsIgnoreCase("optifine") || groupId.equalsIgnoreCase("net.optifine")) && artifactId.equalsIgnoreCase("optifine"))
optiFine = library;
for (LibraryType type : LibraryType.values()) {
if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) {
libraries.put(type, library);
break;
}
}
}
return new LibraryAnalyzer(forge, liteLoader, optiFine);
return new LibraryAnalyzer(libraries);
}
public enum LibraryType {
FORGE(true, Pattern.compile("net\\.minecraftforge"), Pattern.compile("forge")),
LITELOADER(true, Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
OPTIFINE(false, Pattern.compile("(net\\.)?optifine"), Pattern.compile(".*")),
FABRIC(true, Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader"));
private final Pattern group, artifact;
private final boolean modLoader;
LibraryType(boolean modLoader, Pattern group, Pattern artifact) {
this.modLoader = modLoader;
this.group = group;
this.artifact = artifact;
}
public boolean isModLoader() {
return modLoader;
}
}
}
@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.TaskResult;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
public class MaintainTask extends TaskResult<Version> {
private final Version version;
@@ -47,20 +49,20 @@ public class MaintainTask extends TaskResult<Version> {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
if (!libraryAnalyzer.hasForge()) {
if (!libraryAnalyzer.has(FORGE)) {
builder.removeTweakClass("forge");
}
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
builder.removeTweakClass("liteloader");
if (libraryAnalyzer.hasLiteLoader()) {
if (libraryAnalyzer.has(LITELOADER)) {
builder.addArgument("--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
builder.removeTweakClass("optifine");
if (libraryAnalyzer.hasOptiFine()) {
if (!libraryAnalyzer.hasLiteLoader() && !libraryAnalyzer.hasForge()) {
if (libraryAnalyzer.has(OPTIFINE)) {
if (!libraryAnalyzer.has(LITELOADER) && !libraryAnalyzer.has(FORGE)) {
builder.addArgument("--tweakClass", "optifine.OptiFineTweaker");
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
@@ -0,0 +1,37 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download;
public class VersionMismatchException extends Exception {
private final String expect, actual;
public VersionMismatchException(String expect, String actual) {
super("Mismatched game version requirement, library requires game to be " + expect + ", but actual is " + actual);
this.expect = expect;
this.actual = actual;
}
public String getExpect() {
return expect;
}
public String getActual() {
return actual;
}
}
@@ -18,17 +18,26 @@
package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
*
@@ -90,4 +99,36 @@ public final class ForgeInstallTask extends TaskResult<Version> {
else
dependency = new ForgeOldInstallTask(dependencyManager, version, installer);
}
/**
* Install Forge library from existing local file.
*
* @param dependencyManager game repository
* @param version version.json
* @param installer the Forge installer, either the new or old one.
* @return the task to install library
* @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want.
* @throws VersionMismatchException if required game version of installer does not match the actual one.
*/
public static TaskResult<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
Optional<String> gameVersion = GameVersion.minecraftVersion(dependencyManager.getGameRepository().getVersionJar(version));
if (!gameVersion.isPresent()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
String installProfileText = FileUtils.readText(fs.getPath("install_profile.json"));
Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
if (installProfile.containsKey("spec")) {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
if (!gameVersion.get().equals(profile.getMinecraft()))
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
return new ForgeNewInstallTask(dependencyManager, version, installer);
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
throw new VersionMismatchException(profile.getInstall().getMinecraft(), gameVersion.get());
return new ForgeOldInstallTask(dependencyManager, version, installer);
} else {
throw new IOException();
}
}
}
}
@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.download.forge;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import java.util.Collections;
import java.util.HashMap;
@@ -29,7 +32,7 @@ import java.util.Map;
import java.util.stream.Collectors;
@Immutable
public class ForgeNewInstallProfile {
public class ForgeNewInstallProfile implements Validation {
private final int spec;
private final String minecraft;
@@ -97,6 +100,8 @@ public class ForgeNewInstallProfile {
/**
* Data for processors.
*
* @return a mutable data map for processors.
*/
public Map<String, String> getData() {
if (data == null)
@@ -105,7 +110,13 @@ public class ForgeNewInstallProfile {
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
}
public static class Processor {
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (minecraft == null || json == null || path == null)
throw new JsonParseException("ForgeNewInstallProfile is malformed");
}
public static class Processor implements Validation {
private final List<String> sides;
private final Artifact jar;
private final List<Artifact> classpath;
@@ -170,6 +181,12 @@ public class ForgeNewInstallProfile {
public Map<String, String> getOutputs() {
return outputs == null ? Collections.emptyMap() : outputs;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (jar == null)
throw new JsonParseException("Processor::jar cannot be null");
}
}
public static class Datum {
@@ -143,7 +143,7 @@ public class ForgeNewInstallTask extends TaskResult<Version> {
data.put(key, parseLiteral(value,
Collections.emptyMap(),
str -> {
Path dest = temp.resolve(str);
Path dest = Files.createTempFile(temp, null, null);
FileUtils.copyFile(fs.getPath(str), dest);
return dest.toString();
}));
@@ -18,17 +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.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.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.
@@ -40,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
@@ -65,8 +80,10 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
}
@Override
public void execute() {
if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
public void execute() throws IOException {
if (!Arrays.asList("net.minecraft.client.main.Main",
"net.minecraft.launchwrapper.Launch")
.contains(version.getMainClass()))
throw new UnsupportedOptiFineInstallationException();
String remoteVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
@@ -78,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);
@@ -95,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);
}
}
}
@@ -72,6 +72,10 @@ public final class EventManager<T extends Event> {
return Event.Result.DEFAULT;
}
private synchronized void removeConsumer(Consumer<T> consumer) {
handlers.removeValue(consumer);
}
private class WeakListener implements Consumer<T> {
private final WeakReference<Consumer<T>> ref;
@@ -83,7 +87,7 @@ public final class EventManager<T extends Event> {
public void accept(T t) {
Consumer<T> listener = ref.get();
if (listener == null) {
handlers.removeValue(this);
removeConsumer(this);
} else {
listener.accept(t);
}
@@ -166,25 +166,29 @@ public class DefaultGameRepository implements GameRepository {
if (!file.renameTo(removedFile))
return false;
versions.remove(id);
if (FileUtils.isMovingToTrashSupported() && FileUtils.moveToTrash(removedFile)) {
return true;
}
// remove json files first to ensure HMCL will not recognize this folder as a valid version.
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
jsons.forEach(f -> {
if (!f.delete())
LOG.warning("Unable to delete file " + f);
});
// remove the version from version list regardless of whether the directory was removed successfully or not.
try {
FileUtils.deleteDirectory(removedFile);
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
versions.remove(id);
if (FileUtils.isMovingToTrashSupported() && FileUtils.moveToTrash(removedFile)) {
return true;
}
// remove json files first to ensure HMCL will not recognize this folder as a valid version.
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
jsons.forEach(f -> {
if (!f.delete())
LOG.warning("Unable to delete file " + f);
});
// remove the version from version list regardless of whether the directory was removed successfully or not.
try {
FileUtils.deleteDirectory(removedFile);
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
}
return true;
} finally {
refreshVersionsAsync().start();
}
return true;
}
protected void refreshVersionsImpl() {
@@ -217,11 +217,19 @@ public class DefaultLauncher extends Launcher {
public void decompressNatives(File destination) throws NotDecompressingNativesException {
try {
FileUtils.cleanDirectoryQuietly(destination);
for (Library library : version.getLibraries())
if (library.isNative())
new Unzipper(repository.getLibraryFile(version, library), destination)
.setFilter((destFile, isDirectory, zipEntry, path) -> library.getExtract().shouldExtract(path))
.setReplaceExistentFile(true).unzip();
.setFilter((zipEntry, isDirectory, destFile, path) -> {
if (!isDirectory && Files.isRegularFile(destFile) && Files.size(destFile) == Files.size(zipEntry))
return false;
String ext = FileUtils.getExtension(destFile);
if (ext.equals("sha1") || ext.equals("git"))
return false;
return library.getExtract().shouldExtract(path);
})
.setReplaceExistentFile(false).unzip();
} catch (IOException e) {
throw new NotDecompressingNativesException(e);
}
@@ -247,7 +255,7 @@ public class DefaultLauncher extends Launcher {
@Override
public ManagedProcess launch() throws IOException, InterruptedException {
File nativeFolder = Files.createTempDirectory("minecraft").toFile();
File nativeFolder = repository.getNativeDirectory(versionId);
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -27,7 +26,6 @@ import javafx.collections.ObservableList;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
@@ -254,54 +252,6 @@ public class Datapack {
}
}
private static class PackMcMeta implements Validation {
@SerializedName("pack")
private final PackInfo pack;
public PackMcMeta() {
this(new PackInfo());
}
public PackMcMeta(PackInfo packInfo) {
this.pack = packInfo;
}
public PackInfo getPackInfo() {
return pack;
}
@Override
public void validate() throws JsonParseException {
if (pack == null)
throw new JsonParseException("pack cannot be null");
}
public static class PackInfo {
@SerializedName("pack_format")
private final int packFormat;
@SerializedName("description")
private final String description;
public PackInfo() {
this(0, "");
}
public PackInfo(int packFormat, String description) {
this.packFormat = packFormat;
this.description = description;
}
public int getPackFormat() {
return packFormat;
}
public String getDescription() {
return description;
}
}
}
private static final String DISABLED_EXT = "disabled";
}
@@ -0,0 +1,94 @@
/*
* 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.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Immutable
public final class FabricModMetadata {
private final String name;
private final String version;
private final String description;
private final List<FabricModAuthor> authors;
private final Map<String, String> contact;
public FabricModMetadata() {
this("", "", "", Collections.emptyList(), Collections.emptyMap());
}
public FabricModMetadata(String name, String version, String description, List<FabricModAuthor> authors, Map<String, String> contact) {
this.name = name;
this.version = version;
this.description = description;
this.authors = authors;
this.contact = contact;
}
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("fabric.mod.json");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Fabric mod.");
FabricModMetadata metadata = JsonUtils.fromNonNullJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), FabricModMetadata.class);
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
return new ModInfo(modManager, modFile, metadata.name, metadata.description,
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "");
}
}
@JsonAdapter(FabricModAuthorSerializer.class)
public static final class FabricModAuthor {
private final String name;
public FabricModAuthor() {
this("");
}
public FabricModAuthor(String name) {
this.name = name;
}
}
public static final class FabricModAuthorSerializer implements JsonSerializer<FabricModAuthor>, JsonDeserializer<FabricModAuthor> {
@Override
public FabricModAuthor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return json.isJsonPrimitive() ? new FabricModAuthor(json.getAsString()) : new FabricModAuthor(json.getAsJsonObject().getAsJsonPrimitive("name").getAsString());
}
@Override
public JsonElement serialize(FabricModAuthor src, Type typeOfSrc, JsonSerializationContext context) {
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.name);
}
}
}
@@ -64,7 +64,12 @@ public final class ModManager {
}
try {
return RiftModMetadata.fromFile(this, modFile);
return FabricModMetadata.fromFile(this, modFile);
} catch (Exception ignore) {
}
try {
return PackMcMeta.fromFile(this, modFile);
} catch (Exception ignore) {
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod;
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 java.io.IOException;
import java.io.InputStream;
@@ -264,12 +265,23 @@ public final class MultiMCInstanceConfiguration {
return mmcPack;
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
public static Path getRootPath(Path root) throws IOException {
if (testPath(root)) return root;
Path candidate = Files.list(root).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (testPath(candidate)) return candidate;
throw new IOException("Not a valid MultiMC modpack");
}
public static Modpack readMultiMCModpackManifest(Path modpackFile, Charset encoding) throws IOException {
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile, encoding);
try (FileSystem fs = CompressingUtils.readonly(modpackFile).setEncoding(encoding).build()) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
String name = StringUtils.removeSuffix(root.normalize().getFileName().toString(), "/");
Path root = getRootPath(fs.getPath("/"));
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
String name = FileUtils.getName(root, FileUtils.getNameWithoutExtension(modpackFile));
Path instancePath = root.resolve("instance.cfg");
if (Files.notExists(instancePath))
@@ -52,21 +52,24 @@ public final class MultiMCManifest {
return components;
}
public static MultiMCManifest readMultiMCModpackManifest(Path zipFile, Charset encoding) throws IOException {
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(encoding).build()) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
Path mmcPack = root.resolve("mmc-pack.json");
if (Files.notExists(mmcPack))
return null;
String json = IOUtils.readFullyAsString(Files.newInputStream(mmcPack));
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
/**
* Read MultiMC modpack manifest from zip file
* @param root root path in zip file (Path root is a path of ZipFileSystem)
* @return the MultiMC modpack manifest.
* @throws IOException if zip file is malformed
* @throws com.google.gson.JsonParseException if manifest is malformed.
*/
public static MultiMCManifest readMultiMCModpackManifest(Path root) throws IOException {
Path mmcPack = root.resolve("mmc-pack.json");
if (Files.notExists(mmcPack))
return null;
String json = IOUtils.readFullyAsString(Files.newInputStream(mmcPack));
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
if (manifest.getComponents() == null)
throw new IOException("mmc-pack.json malformed.");
if (manifest.getComponents() == null)
throw new IOException("mmc-pack.json malformed.");
return manifest;
}
return manifest;
}
public static final class MultiMCManifestCachedRequires {
@@ -135,8 +135,7 @@ public final class MultiMCModpackInstallTask extends Task {
Version version = repository.readVersionJson(name);
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
Path patches = root.resolve("patches");
if (Files.exists(patches))
@@ -0,0 +1,98 @@
/*
* 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.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Immutable
public class PackMcMeta implements Validation {
@SerializedName("pack")
private final PackInfo pack;
public PackMcMeta() {
this(new PackInfo());
}
public PackMcMeta(PackInfo packInfo) {
this.pack = packInfo;
}
public PackInfo getPackInfo() {
return pack;
}
@Override
public void validate() throws JsonParseException {
if (pack == null)
throw new JsonParseException("pack cannot be null");
}
public static class PackInfo {
@SerializedName("pack_format")
private final int packFormat;
@SerializedName("description")
private final String description;
public PackInfo() {
this(0, "");
}
public PackInfo(int packFormat, String description) {
this.packFormat = packFormat;
this.description = description;
}
public int getPackFormat() {
return packFormat;
}
public String getDescription() {
return description;
}
}
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("pack.mcmeta");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a resource pack.");
PackMcMeta metadata = JsonUtils.fromNonNullJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), PackMcMeta.class);
return new ModInfo(modManager, modFile, metadata.pack.description, "", "", "", "", "");
}
}
}
@@ -1,74 +0,0 @@
/*
* 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.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
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.Collections;
import java.util.List;
@Immutable
public final class RiftModMetadata {
private final String id;
private final String name;
private final List<String> authors;
public RiftModMetadata() {
this("", "", Collections.emptyList());
}
public RiftModMetadata(String id, String name, List<String> authors) {
this.id = id;
this.name = name;
this.authors = authors;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public List<String> getAuthors() {
return authors;
}
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("riftmod.json");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
RiftModMetadata metadata = JsonUtils.fromNonNullJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), RiftModMetadata.class);
String authors = metadata.getAuthors() == null ? "" : String.join(", ", metadata.getAuthors());
return new ModInfo(modManager, modFile, metadata.getName(), "",
authors, "", "", "");
}
}
}
@@ -21,10 +21,7 @@ import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.*;
import java.io.File;
import java.io.IOException;
@@ -233,7 +230,7 @@ public class FileDownloadTask extends Task {
repository.removeRemoteEntry(con);
}
} else if (con.getResponseCode() / 100 != 2) {
throw new IOException("Server error, response code: " + con.getResponseCode());
throw new ResponseCodeException(currentURL, con.getResponseCode());
}
int contentLength = con.getContentLength();
@@ -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.
*
@@ -100,6 +100,11 @@ public final class FileUtils {
return StringUtils.removeSuffix(path.getFileName().toString(), "/", "\\");
}
public static String getName(Path path, String candidate) {
if (path.getFileName() == null) return candidate;
else return getName(path);
}
public static String readText(File file) throws IOException {
return readText(file, UTF_8);
}
@@ -277,6 +282,15 @@ public final class FileUtils {
throw exception;
}
public static boolean cleanDirectoryQuietly(File directory) {
try {
cleanDirectory(directory);
return true;
} catch (IOException e) {
return false;
}
}
public static void forceDelete(File file)
throws IOException {
if (file.isDirectory()) {
@@ -0,0 +1,41 @@
/*
* 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.util.io;
import java.io.IOException;
import java.net.URL;
public class ResponseCodeException extends IOException {
private final URL url;
private final int responseCode;
public ResponseCodeException(URL url, int responseCode) {
super("Unable to request url " + url + ", response code: " + responseCode);
this.url = url;
this.responseCode = responseCode;
}
public URL getUrl() {
return url;
}
public int getResponseCode() {
return responseCode;
}
}
@@ -117,8 +117,12 @@ public class Unzipper {
Path destFile = dest.resolve(relativePath);
if (filter != null && !filter.accept(file, false, destFile, relativePath))
return FileVisitResult.CONTINUE;
if (replaceExistentFile || Files.notExists(destFile))
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
try {
Files.copy(file, destFile, replaceExistentFile ? new CopyOption[]{StandardCopyOption.REPLACE_EXISTING} : new CopyOption[]{});
} catch (FileAlreadyExistsException e) {
if (replaceExistentFile)
throw e;
}
return FileVisitResult.CONTINUE;
}
@@ -128,10 +132,8 @@ public class Unzipper {
String relativePath = root.relativize(dir).toString();
Path dirToCreate = dest.resolve(relativePath);
if (filter != null && !filter.accept(dir, true, dirToCreate, relativePath))
return FileVisitResult.CONTINUE;
if (Files.notExists(dirToCreate)) {
Files.createDirectory(dirToCreate);
}
return FileVisitResult.SKIP_SUBTREE;
Files.createDirectories(dirToCreate);
return FileVisitResult.CONTINUE;
}
});
@@ -139,6 +141,6 @@ public class Unzipper {
}
public interface FileFilter {
boolean accept(Path destPath, boolean isDirectory, Path zipEntry, String entryPath) throws IOException;
boolean accept(Path zipEntry, boolean isDirectory, Path destFile, String entryPath) throws IOException;
}
}
@@ -132,6 +132,14 @@ public class ObservableCache<K, V, E extends Exception> {
}
public ObjectBinding<V> binding(K key) {
return binding(key, false);
}
/**
* @param quiet if true, calling get() on the returned binding won't toggle a query
*/
public ObjectBinding<V> binding(K key, boolean quiet) {
// This method is thread-safe because ObservableHelper supports concurrent modification
return Bindings.createObjectBinding(() -> {
V result;
boolean refresh;
@@ -144,7 +152,7 @@ public class ObservableCache<K, V, E extends Exception> {
refresh = invalidated.containsKey(key);
}
}
if (refresh) {
if (!quiet && refresh) {
query(key, executor);
}
return result;
@@ -56,6 +56,10 @@ public class ObservableOptionalCache<K, V, E extends Exception> {
return backed.binding(key);
}
public ObjectBinding<Optional<V>> binding(K key, boolean quiet) {
return backed.binding(key, quiet);
}
public void invalidate(K key) {
backed.invalidate(key);
}