Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a141c6b137 | ||
|
|
300efb99d2 | ||
|
|
6fb3610bcc | ||
|
|
43fb3ca10a | ||
|
|
adc360eaaa | ||
|
|
5e659352d7 | ||
|
|
6595e0a3cf | ||
|
|
c43b2b3bb9 | ||
|
|
ea19e6efd8 | ||
|
|
55d38f2b36 | ||
|
|
e0abf3e0c1 | ||
|
|
974cba8dee | ||
|
|
019cf5fc8f | ||
|
|
1956ba7a73 | ||
|
|
c209617ee7 | ||
|
|
f4b5e061a4 | ||
|
|
06b67a4b9a | ||
|
|
a385265956 | ||
|
|
6ac32a40c7 | ||
|
|
20c612c660 | ||
|
|
7e8cc5fe6c | ||
|
|
9ed14b227b | ||
|
|
ee466d0f4c | ||
|
|
b198eea9e4 | ||
|
|
8c7580488e | ||
|
|
3f4364c67c | ||
|
|
23e2df5e58 | ||
|
|
79136db162 |
No files matched your search
+1
-1
@@ -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=正在检查更新
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
+5
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -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();
|
||||
}));
|
||||
|
||||
+63
-9
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user