Compare commits

..
34 Commits
Author SHA1 Message Date
huanghongxun f4f89fac1f Merge pull request #438 from yushijinhun/force-update
移除「更新一次 HMCL,电脑上所有 HMCL 都得到更新」的功能
2018-08-26 19:51:20 +08:00
yushijinhun 5262150d3c Prevent 2.x being updated to 3.x 2018-08-26 19:34:10 +08:00
yushijinhun b5f56eb2b5 Remove force update feature 2018-08-26 19:02:47 +08:00
huangyuhui 9a375fd0d1 Add hiding versions. Closes #437 2018-08-26 11:50:42 +08:00
huangyuhui 6ca4aeb5a2 Support 1.13 auto-installing 2018-08-26 11:48:27 +08:00
huangyuhui 8ed4a9a5f2 Fix not showing Optifine 1.13 alpha 2018-08-25 19:53:29 +08:00
huangyuhui a4578405cf Remove RandomUserAgent 2018-08-25 19:39:00 +08:00
huangyuhui 73f147b7e2 Update Chinese localization 2018-08-23 21:45:56 +08:00
huangyuhui 5c3d735315 Prevent HMCL 2.x updating 2018-08-23 21:45:47 +08:00
huanghongxun 200621996d Merge pull request #431 from pan93412/javafx
Update Chinese (Taiwan) Translation
2018-08-22 20:56:12 +08:00
byStarTW a912a0a07e Update Chinese (Taiwan) Translation 2018-08-22 20:54:49 +08:00
huangyuhui 4146ceb048 Fix not downloading the correct game version 2018-08-22 04:25:35 +08:00
huangyuhui ad568d2b9f Fix #429 2018-08-22 02:08:49 +08:00
huangyuhui c7bfb9a11d Suggest user to install latest Java 8 to avoid potentional game crashing 2018-08-21 21:12:05 +08:00
huangyuhui c9391181bd Display empty message when loading 1.13 forge version list 2018-08-21 19:31:04 +08:00
huangyuhui b08eb519bf Remove logging support which is meaningless 2018-08-21 11:48:19 +08:00
huangyuhui 47c4fc2f7e Fix #426 2018-08-20 11:53:02 +08:00
huangyuhui dfd177433a Fix not replacing tokens metioned in #426 2018-08-20 11:38:06 +08:00
huanghongxun 27381e9696 Merge pull request #425 from yushijinhun/yushijinhun/javafx
修复 UpdateChecker 潜在问题 & 弃用 ImmediateXXXProperty
2018-08-19 19:47:30 +08:00
yushijinhun 1c7827c53c Deprecate and partially remove ImmediateProperty 2018-08-19 15:55:18 +08:00
yushijinhun 9512bdb5d6 Remove UpdateChecker.updateChannel & Fix concurrent problems in update checking 2018-08-19 15:43:34 +08:00
huangyuhui 33d1f1d692 Not showing 'unknown' in mod info 2018-08-19 12:19:11 +08:00
huangyuhui c6b49545ff Fix #424 2018-08-19 12:13:39 +08:00
huangyuhui a4a833bfdb Fix auto-installing: missing checksums in game json 2018-08-19 11:58:02 +08:00
huangyuhui 2e1b6e4630 Fix not correctly loading remote game version list 2018-08-19 11:56:39 +08:00
huangyuhui 6911cfa387 try to move deleting version to trash 2018-08-19 11:34:07 +08:00
huangyuhui aa29f890de Log memory size and metaspace 2018-08-19 02:03:05 +08:00
huangyuhui 0f4e6a01bc Fix unable to install more than one libraries 2018-08-19 01:18:57 +08:00
huangyuhui 1d3d8d6710 Show OptiFine versions in dividually 2018-08-19 00:39:47 +08:00
huangyuhui 005e06c56f Always download Optifine from BMCLAPI 2018-08-19 00:35:37 +08:00
huangyuhui 0a7c32b3cb Friendly prompt warning user about independent version deletion 2018-08-18 22:40:08 +08:00
huangyuhui 82916a04ea Check if forge libraries' checksums are valid 2018-08-18 22:25:13 +08:00
huangyuhui ba2e419894 Fix #422 2018-08-18 22:09:16 +08:00
huangyuhui 0812c9025d Do not check game asset hash code for every launching 2018-08-18 02:00:00 +08:00
75 changed files with 1140 additions and 3345 deletions

No files matched your search

@@ -17,23 +17,20 @@
*/
package org.jackhuang.hmcl;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
@@ -41,7 +38,6 @@ import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public final class Launcher extends Application {
@@ -57,24 +53,7 @@ public final class Launcher extends Application {
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UpdateChecker.updateChannelProperty().addListener(observable -> {
thread(() -> {
try {
UpdateChecker.checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
});
});
UpdateChecker.updateChannelProperty().bind(Bindings.createStringBinding(() -> {
switch (ConfigHolder.config().getUpdateChannel()) {
case DEVELOPMENT:
return UpdateChecker.CHANNEL_DEV;
default:
return UpdateChecker.CHANNEL_STABLE;
}
}, ConfigHolder.config().updateChannelProperty()));
UpdateChecker.init();
primaryStage.show();
} catch (Throwable e) {
@@ -101,6 +80,9 @@ public final class Launcher extends Application {
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -227,29 +227,50 @@ public final class LauncherHelper {
flag = true;
}
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
}
flag = true;
}
flag = true;
}
// LaunchWrapper will crash because of assuming the system class loader is an instance of URLClassLoader.
// cpw has claimed that he will make MinecraftForge of 1.13 and later versions able to run on Java 9.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.13")) < 0 && version.getMainClass().contains("launchwrapper")) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
} else {
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
flag = true;
}
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
@@ -257,6 +278,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
@@ -264,6 +286,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
@@ -30,6 +30,7 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
@@ -150,7 +151,7 @@ public final class Config implements Cloneable, Observable {
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("updateChannel")
private ObjectProperty<EnumUpdateChannel> updateChannel = new SimpleObjectProperty<>(EnumUpdateChannel.STABLE);
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -438,15 +439,15 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public EnumUpdateChannel getUpdateChannel() {
public UpdateChannel getUpdateChannel() {
return updateChannel.get();
}
public ObjectProperty<EnumUpdateChannel> updateChannelProperty() {
public ObjectProperty<UpdateChannel> updateChannelProperty() {
return updateChannel;
}
public void setUpdateChannel(EnumUpdateChannel updateChannel) {
public void setUpdateChannel(UpdateChannel updateChannel) {
this.updateChannel.set(updateChannel);
}
}
@@ -21,6 +21,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -35,6 +36,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.LinkedList;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
public class InstallerController {
private Profile profile;
@@ -43,9 +45,6 @@ public class InstallerController {
@FXML
private ScrollPane scrollPane;
@FXML private VBox contentPane;
private String forge;
private String liteLoader;
private String optiFine;
@FXML
private void initialize() {
@@ -58,32 +57,22 @@ public class InstallerController {
this.version = profile.getRepository().getResolvedVersion(versionId);
contentPane.getChildren().clear();
forge = liteLoader = optiFine = null;
for (Library library : version.getLibraries()) {
Consumer<InstallerItem> removeAction = x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction));
forge = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction));
liteLoader = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction));
optiFine = library.getVersion();
}
}
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
analyzer.getForge().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction.apply(library))));
}
@FXML
@@ -93,6 +82,6 @@ public class InstallerController {
if (!gameVersion.isPresent())
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version));
}
}
@@ -30,6 +30,7 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -119,17 +120,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n"));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n"));
analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n"));
item.setLibraries(libraries.toString());
});
@@ -236,6 +230,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
@@ -46,6 +46,7 @@ import org.jackhuang.hmcl.ui.construct.FontComboBox;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
@@ -242,24 +243,24 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
lblUpdateNote.setWrappingWidth(470);
ObjectProperty<EnumUpdateChannel> updateChannel = new SimpleObjectProperty<EnumUpdateChannel>() {
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
@Override
protected void invalidated() {
EnumUpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == EnumUpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == EnumUpdateChannel.STABLE);
UpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
}
};
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(EnumUpdateChannel.DEVELOPMENT);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(EnumUpdateChannel.STABLE);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
updateChannelGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateChannel.set((EnumUpdateChannel) toggle.getUserData());
updateChannel.set((UpdateChannel) toggle.getUserData());
}
}));
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
@@ -26,16 +26,17 @@ import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", null);
@@ -186,7 +187,12 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(i18n("version.manage.remove.confirm", version), i18n("message.confirm"), () -> {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
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();
Controllers.navigate(null);
@@ -103,7 +103,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion) it).map(RemoteVersion::getSelfVersion).orElse(null);
}
@Override
@@ -49,16 +49,16 @@ public final class DownloadWizardProvider implements WizardProvider {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version((RemoteVersion<?>) settings.get("forge"));
builder.version((RemoteVersion) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version((RemoteVersion<?>) settings.get("liteloader"));
builder.version((RemoteVersion) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version((RemoteVersion<?>) settings.get("optifine"));
builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
}
@@ -21,7 +21,9 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
@@ -40,16 +42,14 @@ public final class InstallerWizardProvider implements WizardProvider {
private final String optiFine;
public InstallerWizardProvider(Profile profile, String gameVersion, Version version) {
this(profile, gameVersion, version, null, null, null);
}
public InstallerWizardProvider(Profile profile, String gameVersion, Version version, String forge, String liteLoader, String optiFine) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
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);
}
public Profile getProfile() {
@@ -85,16 +85,16 @@ public final class InstallerWizardProvider implements WizardProvider {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
Task ret = Task.empty();
Task ret = Task.ofResult("version", () -> version);
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -74,7 +74,7 @@ public class InstallersPage extends StackPane implements WizardPage {
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
String gameVersion = ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
@@ -103,7 +103,7 @@ public class InstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
}
@Override
@@ -27,9 +27,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -48,6 +47,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private final DownloadProvider downloadProvider;
private final String libraryId;
private final String title;
private final WizardController controller;
@FXML
private JFXListView<VersionsPageItem> list;
@@ -56,6 +56,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private StackPane failedPane;
@FXML
private StackPane emptyPane;
@FXML
private JFXCheckBox chkRelease;
@FXML
private JFXCheckBox chkSnapshot;
@@ -75,11 +77,12 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
this.gameVersion = gameVersion;
this.downloadProvider = downloadProvider;
this.libraryId = libraryId;
this.controller = controller;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
if (versionList instanceof GameVersionList) {
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else
centrePane.getChildren().setAll(list);
@@ -99,21 +102,19 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
private List<VersionsPageItem> loadVersions() {
boolean isGameVersionList = versionList instanceof GameVersionList;
return versionList.getVersions(gameVersion).stream()
.filter(it -> {
if (isGameVersionList)
switch (((GameRemoteVersionTag) it.getTag()).getType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
default:
return chkOld.isSelected();
}
else return true;
switch (it.getVersionType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
case OLD:
return chkOld.isSelected();
default:
return true;
}
})
.filter(Objects::nonNull)
.sorted()
.map(VersionsPageItem::new).collect(Collectors.toList());
}
@@ -126,8 +127,18 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
List<VersionsPageItem> items = loadVersions();
Platform.runLater(() -> {
list.getItems().setAll(items);
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
if (versionList.getVersions(gameVersion).isEmpty()) {
transitionHandler.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
if (items.isEmpty()) {
chkRelease.setSelected(true);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
}
});
} else {
Platform.runLater(() -> {
@@ -153,4 +164,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private void onRefresh() {
refresh();
}
@FXML
private void onBack() { controller.onPrev(true); }
}
@@ -25,8 +25,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersionTag;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.ui.FXUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -37,7 +38,7 @@ import java.util.Objects;
* @author huangyuhui
*/
public final class VersionsPageItem extends StackPane {
private final RemoteVersion<?> remoteVersion;
private final RemoteVersion remoteVersion;
@FXML
private Label lblSelfVersion;
@FXML
@@ -49,14 +50,14 @@ public final class VersionsPageItem extends StackPane {
@FXML
private StackPane imageViewContainer;
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
public VersionsPageItem(RemoteVersion remoteVersion) {
this.remoteVersion = Objects.requireNonNull(remoteVersion);
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
lblSelfVersion.setText(remoteVersion.getSelfVersion());
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
lblGameVersion.setText(i18n("version.game.release"));
imageView.setImage(new Image("/assets/img/icon.png", 32, 32, false, true));
@@ -70,13 +71,13 @@ public final class VersionsPageItem extends StackPane {
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
}
} else if (remoteVersion.getTag() instanceof LiteLoaderRemoteVersionTag) {
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
imageView.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
// optifine has no icon.
lblGameVersion.setText(remoteVersion.getGameVersion());
} else { // forge
} else if (remoteVersion instanceof ForgeRemoteVersion) {
imageView.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
}
@@ -84,7 +85,7 @@ public final class VersionsPageItem extends StackPane {
leftPane.getChildren().remove(imageViewContainer);
}
public RemoteVersion<?> getRemoteVersion() {
public RemoteVersion getRemoteVersion() {
return remoteVersion;
}
}
@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
class HMCLDownloadTask extends FileDownloadTask {
private RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
archiveFormat = version.getType();
}
@Override
public void execute() throws Exception {
super.execute();
try {
Path target = getFile().toPath();
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200.newUnpacker().unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
getFile().delete();
throw e;
}
}
}
@@ -37,6 +37,7 @@ import java.util.zip.ZipFile;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.JarUtils;
/**
* A class that checks the integrity of HMCL.
@@ -123,8 +124,7 @@ public final class IntegrityChecker {
}
private static void verifySelf() throws IOException {
Path self = LocalVersion.current().orElseThrow(() -> new IOException("Failed to find myself"))
.getLocation();
Path self = JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
requireVerifiedJar(self);
}
}
@@ -1,162 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.JarUtils;
import org.tukaani.xz.XZInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* A class used to manage the local HMCL repository.
*
* @author yushijinhun
*/
final class LocalRepository {
private LocalRepository() {}
private static Path localStorage = Launcher.HMCL_DIRECTORY.toPath().resolve("hmcl.jar");
private static Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
/**
* Gets the current stored executable in local repository.
*/
public static Optional<LocalVersion> getStored() {
if (!Files.isRegularFile(localStorage)) {
return Optional.empty();
}
return Optional.of(localStorage)
.flatMap(JarUtils::getImplementationVersion)
.map(version -> new LocalVersion(version, localStorage));
}
private static void writeToStorage(Path source, boolean checkHeaders) throws IOException {
IntegrityChecker.requireVerifiedJar(source);
Files.createDirectories(localStorage.getParent());
if (checkHeaders) {
ExecutableHeaderHelper.copyWithoutHeader(source, localStorage);
} else {
Files.copy(source, localStorage, StandardCopyOption.REPLACE_EXISTING);
}
Optional<LocalVersion> stored = getStored();
if (stored.isPresent()) {
Map<String, String> json = new HashMap<>();
json.put("ver", stored.get().getVersion());
json.put("loc", stored.get().getLocation().toString());
FileUtils.writeText(hmclVersionJson.toFile(), Constants.GSON.toJson(json));
}
}
/**
* Creates a task that downloads the given version to local repository.
*/
public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException {
Path downloaded = Files.createTempFile("hmcl-update-", null);
return new FileDownloadTask(new URL(version.getUrl()), downloaded.toFile(), version.getIntegrityCheck()) {
@Override
public void execute() throws Exception {
super.execute();
try {
switch (version.getType()) {
case JAR:
writeToStorage(downloaded, false);
break;
case PACK_XZ:
Path unpacked = Files.createTempFile("hmcl-update-unpack-", null);
try {
try (InputStream in = new XZInputStream(Files.newInputStream(downloaded));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) {
Pack200.newUnpacker().unpack(in, out);
}
writeToStorage(unpacked, false);
} finally {
Files.deleteIfExists(unpacked);
}
break;
default:
throw new IllegalArgumentException("Unknown type: " + version.getType());
}
} finally {
Files.deleteIfExists(downloaded);
}
}
};
}
/**
* Copies the current HMCL executable to local repository.
*/
public static void downloadFromCurrent() {
Optional<LocalVersion> current = LocalVersion.current();
if (current.isPresent()) {
Path currentPath = current.get().getLocation();
if (!Files.isRegularFile(currentPath)) {
LOG.warning("Failed to download " + current.get() + ", it isn't a file");
return;
}
if (isSameAsLocalStorage(currentPath)) {
LOG.warning("Trying to download from self, ignored");
return;
}
LOG.info("Downloading " + current.get());
try {
writeToStorage(current.get().getLocation(), true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download " + current.get(), e);
}
}
}
/**
* Writes the executable stored in local repository to the given location.
*/
public static void applyTo(Path target) throws IOException {
if (isSameAsLocalStorage(target)) {
throw new IOException("Cannot apply update to self");
}
LOG.info("Applying update to " + target);
IntegrityChecker.requireVerifiedJar(localStorage);
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
}
private static boolean isSameAsLocalStorage(Path path) {
return path.toAbsolutePath().equals(localStorage.toAbsolutePath());
}
}
@@ -1,52 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.JarUtils;
import java.nio.file.Path;
import java.util.Optional;
class LocalVersion {
public static Optional<LocalVersion> current() {
return JarUtils.thisJar().map(path -> new LocalVersion(Metadata.VERSION, path));
}
private String version;
private Path location;
public LocalVersion(String version, Path location) {
this.version = version;
this.location = location;
}
public String getVersion() {
return version;
}
public Path getLocation() {
return location;
}
@Override
public String toString() {
return "[" + version + " at " + location + "]";
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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
@@ -15,9 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.setting;
package org.jackhuang.hmcl.upgrade;
public enum EnumUpdateChannel {
STABLE,
DEVELOPMENT
public enum UpdateChannel {
STABLE("stable"),
DEVELOPMENT("dev");
public final String channelName;
UpdateChannel(String channelName) {
this.channelName = channelName;
}
}
@@ -17,16 +17,20 @@
*/
package org.jackhuang.hmcl.upgrade;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
import java.io.IOException;
import java.util.logging.Level;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.ImmediateStringProperty;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.NetworkUtils;
import javafx.application.Platform;
@@ -37,11 +41,6 @@ import javafx.beans.value.ObservableBooleanValue;
public final class UpdateChecker {
private UpdateChecker() {}
public static final String CHANNEL_STABLE = "stable";
public static final String CHANNEL_DEV = "dev";
private static StringProperty updateChannel = new ImmediateStringProperty(null, "updateChannel", CHANNEL_STABLE);
private static ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static BooleanBinding outdated = Bindings.createBooleanBinding(
() -> {
@@ -55,16 +54,9 @@ public final class UpdateChecker {
latestVersion);
private static ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
public static String getUpdateChannel() {
return updateChannel.get();
}
public static void setUpdateChannel(String updateChannel) {
UpdateChecker.updateChannel.set(updateChannel);
}
public static StringProperty updateChannelProperty() {
return updateChannel;
public static void init() {
ConfigHolder.config().updateChannelProperty().addListener(onInvalidating(UpdateChecker::requestCheckUpdate));
requestCheckUpdate();
}
public static RemoteVersion getLatestVersion() {
@@ -91,35 +83,53 @@ public final class UpdateChecker {
return checkingUpdate.getReadOnlyProperty();
}
public static void checkUpdate() throws IOException {
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
if (!IntegrityChecker.isSelfVerified()) {
return;
throw new IOException("Self verification failed");
}
JFXUtilities.runInFXAndWait(() -> {
checkingUpdate.set(true);
});
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel.channelName)));
try {
String channel = getUpdateChannel();
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel)));
RemoteVersion fetched = RemoteVersion.fetch(url);
Platform.runLater(() -> {
if (channel.equals(getUpdateChannel())) {
latestVersion.set(fetched);
}
});
} finally {
Platform.runLater(() -> checkingUpdate.set(false));
}
return RemoteVersion.fetch(url);
}
private static boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @develop@
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
}
public static void requestCheckUpdate() {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
checkingUpdate.set(true);
UpdateChannel channel = config().getUpdateChannel();
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
RemoteVersion finalResult = result;
Platform.runLater(() -> {
checkingUpdate.set(false);
if (finalResult != null) {
if (channel.equals(config().getUpdateChannel())) {
latestVersion.set(finalResult);
} else {
// the channel has been changed during the period
// check update again
requestCheckUpdate();
}
}
});
});
});
}
}
@@ -17,11 +17,10 @@
*/
package org.jackhuang.hmcl.upgrade;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.IntVersionNumber.isIntVersionNumber;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.IOException;
@@ -30,6 +29,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher;
@@ -45,9 +45,12 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.JarUtils;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.StringUtils;
import com.google.gson.Gson;
import javafx.application.Platform;
import javafx.scene.layout.Region;
@@ -58,9 +61,7 @@ public final class UpdateHandler {
* @return whether to exit
*/
public static boolean processArguments(String[] args) {
if (!isIntVersionNumber(Metadata.VERSION)) {
return false;
}
breakForceUpdateFeature();
if (isNestedApplication()) {
// updated from old versions
@@ -88,33 +89,69 @@ public final class UpdateHandler {
return true;
}
Optional<LocalVersion> local = LocalRepository.getStored();
if (local.isPresent()) {
int difference = asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION));
return false;
}
if (difference < 0) {
LocalRepository.downloadFromCurrent();
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
} else if (difference > 0) {
Optional<LocalVersion> current = LocalVersion.current();
if (current.isPresent() && IntegrityChecker.isSelfVerified()) {
try {
requestUpdate(local.get().getLocation(), current.get().getLocation());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update from local repository", e);
return false;
}
return true;
} else {
return false;
}
}
} else {
LocalRepository.downloadFromCurrent();
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
return false;
Task task = new HMCLDownloadTask(version, downloaded);
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null);
thread(() -> {
boolean success = executor.test();
Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent()));
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"), MessageBox.ERROR_MESSAGE));
return;
}
} else {
Throwable e = task.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
}
});
}
private static void applyUpdate(Path target) throws IOException {
LOG.info("Applying update to " + target);
Path self = getCurrentLocation();
IntegrityChecker.requireVerifiedJar(self);
ExecutableHeaderHelper.copyWithHeader(self, target);
Optional<Path> newFilename = tryRename(target, Metadata.VERSION);
if (newFilename.isPresent()) {
LOG.info("Move " + target + " to " + newFilename.get());
try {
Files.move(target, newFilename.get());
target = newFilename.get();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to move target", e);
}
}
startJava(target);
}
private static void requestUpdate(Path updateTo, Path self) throws IOException {
@@ -122,28 +159,6 @@ public final class UpdateHandler {
startJava(updateTo, "--apply-to", self.toString());
}
private static void applyUpdate(Path target) throws IOException {
LocalRepository.applyTo(target);
Optional<String> newVersion = LocalRepository.getStored().map(LocalVersion::getVersion);
if (newVersion.isPresent()) {
Optional<Path> newFilename = tryRename(target, newVersion.get());
if (newFilename.isPresent()) {
LOG.info("Move " + target + " to " + newFilename.get());
try {
Files.move(target, newFilename.get());
target = newFilename.get();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to move target", e);
}
}
} else {
LOG.warning("Failed to find local repository");
}
startJava(target);
}
private static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath());
@@ -159,49 +174,6 @@ public final class UpdateHandler {
.start();
}
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
Task task;
try {
task = LocalRepository.downloadFromRemote(version);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create upgrade download task", e);
return;
}
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null);
thread(() -> {
boolean success = executor.test();
Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent()));
if (success) {
try {
Optional<LocalVersion> current = LocalVersion.current();
Optional<LocalVersion> stored = LocalRepository.getStored();
if (!current.isPresent()) {
throw new IOException("Failed to find current HMCL location");
}
if (!stored.isPresent()) {
throw new IOException("Failed to find local repository, this shouldn't happen");
}
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(stored.get().getLocation(), current.get().getLocation());
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"), MessageBox.ERROR_MESSAGE));
return;
}
} else {
Throwable e = task.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
}
});
}
private static Optional<Path> tryRename(Path path, String newVersion) {
String filename = path.getFileName().toString();
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
@@ -214,6 +186,10 @@ public final class UpdateHandler {
return Optional.empty();
}
private static Path getCurrentLocation() throws IOException {
return JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
}
// ==== support for old versions ===
private static void performMigration() throws IOException {
LOG.info("Migrating from old versions");
@@ -221,17 +197,7 @@ public final class UpdateHandler {
Path location = getParentApplicationLocation()
.orElseThrow(() -> new IOException("Failed to get parent application location"));
Optional<LocalVersion> local = LocalRepository.getStored();
if (!local.isPresent() ||
asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION)) < 0) {
LocalRepository.downloadFromCurrent();
}
local = LocalRepository.getStored();
if (!local.isPresent()) {
throw new IOException("Failed to find local repository");
}
requestUpdate(local.get().getLocation(), location);
requestUpdate(getCurrentLocation(), location);
}
/**
@@ -265,7 +231,7 @@ public final class UpdateHandler {
}
private static boolean isFirstLaunchAfterUpgrade() {
Optional<Path> currentPath = LocalVersion.current().map(LocalVersion::getLocation);
Optional<Path> currentPath = JarUtils.thisJar();
if (currentPath.isPresent()) {
Path updated = Launcher.HMCL_DIRECTORY.toPath().resolve("HMCL-" + Metadata.VERSION + ".jar");
if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) {
@@ -274,5 +240,21 @@ public final class UpdateHandler {
}
return false;
}
private static void breakForceUpdateFeature() {
try {
Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
Map<?, ?> content = new Gson().fromJson(new String(Files.readAllBytes(hmclVersionJson), UTF_8), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
LOG.info("Successfully broke the force update feature");
}
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to break the force update feature", e);
}
}
// ====
}
@@ -20,4 +20,7 @@
<StackPane fx:id="failedPane">
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane">
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
</StackPane>
</fx:root>
@@ -83,6 +83,7 @@ crash.user_fault=Your OS or Java environment may not be properly installed resul
download=Download
download.BMCL=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
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.
download.mojang=Mojang
@@ -134,6 +135,7 @@ lang.default=Use system language
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
launch.advice.newer_java=Java 8 is suggested, which can make game run more fluently. And many mods and Minecraft 1.12 and newer versions requires Java 8.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game probably crash. The launcher will try to launch it.
@@ -317,7 +319,7 @@ update.checking=Checking for updates
update.failed=Failed to perform upgrade
update.found=Update Available!
update.newest_version=Latest version: %s
update.note=Development version contains more functionality and bug fixes as well as more possible bugs. And this will affect all HMCL installations in your computer.
update.note=Development version contains more functionality and bug fixes as well as more possible bugs.
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
update.tooltip=Update
@@ -334,7 +336,9 @@ version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove version %s?
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
version.manage.remove.confirm.trash=Sure to remove game %s? You can restore the game with the name of %s in your system trash.
version.manage.remove.confirm.independent=Since this game is in independent mode, deleting this game will result in all saved worlds belonging to this game removed simultaneously. Sure to remove game %s?
version.manage.remove_libraries=Delete library files
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
@@ -14,332 +14,336 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see {http://www.gnu.org/licenses/}.
#
#author: huangyuhui
# author: huangyuhui
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鳴謝
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默認背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (預設背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.dependency=依賴
# 由於篇幅限制,僅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免責聲明
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟件產生的版權問題,軟件製作方概不負責。請支持正版。
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟體產生的版權問題,軟體製作方概不負責。請支援正版。
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account=帳戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.create=建立帳戶
account.email=電子信箱
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的使用者名稱或密碼錯誤,或者登入次數過多被暫時禁止登入,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
account.failed.no_character=該帳號沒有角色
account.failed.server_response_malformed=這個服務器不是合法的登錄驗證服務器
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
account.injector.http=警告:此服務器使用不安全的 HTTP 協議,您的密碼在登錄時會被明文傳輸。
account.injector.server=認證服務器
account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.failed.server_response_malformed=這個伺服器不是合法的登入驗證伺服器
account.injector.add=新增認證伺服器
account.injector.manage=管理認證伺服器
account.injector.manage.title=認證伺服器
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
account.injector.server=認證伺服器
account.injector.server_url=伺服器位址
account.injector.server_name=伺服器名稱
account.methods=登入方式
account.methods.authlib_injector=authlib-injector 登入
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.methods.yggdrasil=正版登入
account.missing=沒有帳戶
account.password=密碼
account.username=用戶名
account.username=使用者名稱
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
assets.download_all=驗證資源檔案完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.edit=編輯
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.refresh=重新整理
button.save=儲存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
color.custom=自訂顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.mojang=官方伺服器
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.mod=模組檔案
extension.png=圖片檔案
extension.sh=Bash 腳本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_loading_failure=HMCL 無法加載配置文件。\n請確保 HMCL 對 "%s" 目錄及該目錄下的文件擁有讀寫權限。
fatal.migration_requires_manual_reboot=HMCL 即將完成升級,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們反饋該問題。
fatal.missing_javafx=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證。\n您依然可以使用 HMCL,但將無法連線到部分網站(如使用 Lets Encrypt 證書的站點),這可能會使 HMCL 無法正常運作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_loading_failure=HMCL 無法載入設定檔案。\n請確保 HMCL 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
fatal.migration_requires_manual_reboot=HMCL 即將升級完成,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級程序,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們回報該問題。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
folder.config=設定資料夾
folder.coremod=核心 MOD 模組資料夾
folder.game=遊戲資料夾
folder.mod=MOD 模組資料夾
folder.resourcepacks=資源包資料夾
folder.saves=遊戲存檔資料夾
folder.screenshots=截圖資料夾
general.prompt.empty=(無)
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.email=「使用者名稱」必須是電子信箱格式
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
input.not_empty=必填
input.url=必須為有效連結
install=添加遊戲
install=新增遊戲
install.failed=安裝失敗
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫不安裝 %s,可以點擊此處安裝
install.installer.not_installed=暫時不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
install.installer.version=%s 版本
install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=當前遊戲版本
install.new_game.current_game_version=目前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
lang=繁體中文
lang=正體中文
lang.default=跟隨系統語言
launch.advice.different_platform=您的系統是 64 位的但是 Java 是 32 位的,推薦您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java9=不高於 1.13 的安裝了 Mod 的 Minecraft 不支持 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=檢測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更流暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要需要 Java 8。
launch.advice.not_enough_space=您設置的內存大小過大,由於超過了系統內存大小 %dMB,所以可能影響遊戲體驗或無法啓動遊戲,啓動器仍會嘗試啓動。
launch.advice.too_large_memory_for_32bit=您設置的內存大小過大,由於可能超過了 32 位 Java 的內存分配限制,所以可能無法啓動遊戲,請將內存調至 1024MB 或更小,啓動器仍會嘗試啓動。
launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=偵測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更順暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要 Java 8 版本。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。啟動器仍會嘗試啟動。
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。啟動器仍會嘗試啟動。
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新執行程序時發生錯誤。可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
launch.failed.download_library=無法下載遊戲依賴 %s.
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
launch.state.done=啓動完成
launch.state.logging_in=登錄中
launch.state.modpack=正在下載必要文件
launch.state.waiting_launching=等待遊戲啓動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重置爲默認 Java 路徑。
launch.state.done=啟動完成
launch.state.logging_in=登入中
launch.state.modpack=正在下載必要檔案
launch.state.waiting_launching=等待遊戲啟動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重設為預設 Java 路徑。
launcher=啓動器
launcher.background=背景地址
launcher=啟動器
launcher.background=背景位址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.common_directory.default=默認
launcher.common_directory.disabled=禁用
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
launcher.common_directory=公共資料夾
launcher.common_directory.choose=選擇公共資料夾
launcher.common_directory.default=預設
launcher.common_directory.disabled=停用
launcher.contact=聯絡我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並透過 MCBBS、貼吧、GitHub 或 Minecraft Forum 回報 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已偵測到您的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新您的 Java
login.empty_username=你還未設置用戶名!
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
logwindow.terminate_game=結束遊戲執行程序
logwindow.title=記錄
main_page=主頁
main_page=首頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.downloading=正在下載…
message.error=錯誤
message.info=提示
message.success=已完成
message.info=資訊
message.success=完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支持 Markdown(圖片請用網絡圖)。
modpack.enter_name=給遊戲起個你喜歡的名字
modpack.export=導出整合包
modpack.choose=選擇要安裝的遊戲整合包檔案
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。
modpack.enter_name=給遊戲取個你喜歡的名字
modpack.export=匯出整合包
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 調試輸出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods 配置,如小地圖
modpack.files.config=Mod 模組設定檔案
modpack.files.dumps=NEI 調校輸出
modpack.files.liteconfig=Mod 模組設定檔案
modpack.files.mods=Mod 模組
modpack.files.mods.voxelmods=VoxelMods 設定,如小地圖
modpack.files.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.resourcepacks=資源包 (材質包)
modpack.files.saves=遊戲存檔
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人遊戲服務器列表
modpack.install=安裝%s整合包
modpack.files.scripts=MineTweaker 設定
modpack.files.servers_dat=多人遊戲伺服器列表
modpack.install=安裝 %s 整合包
modpack.installing=正在安裝整合包
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級文件,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,當前遊戲是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.introduction=支援 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級檔案,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,目前遊戲是 %s 整合包,但是提供的整合包更新檔案是 %s 整合包。
modpack.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.not_a_valid_name=整合包名稱無效
modpack.scan=解析整合包
modpack.task.install=導入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式不正確或操作文件失敗。
modpack.task.install=匯入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=該整合包不被支持。僅 HMCL、MultiMC、Curse 整合包受支持。
modpack.unsupported=不支援該整合包。僅 HMCL、MultiMC、Curse 整合包受支援。
modpack.update=正在升級整合包
modpack.wizard=導出整合包嚮導
modpack.wizard.step.1=基本設置
modpack.wizard.step.1.title=設置整合包的主要信息
modpack.wizard.step.2=文件選擇
modpack.wizard.step.2.title=選中你想加到整合包中的文件或文件夾
modpack.wizard=匯出整合包引導
modpack.wizard.step.1=基本設定
modpack.wizard.step.1.title=設定整合包的主要訊息
modpack.wizard.step.2=檔案選擇
modpack.wizard.step.2.title=選中你想加到整合包中的檔案或資料夾
modpack.wizard.step.3=整合包描述
modpack.wizard.step.initialization.exported_version=要導出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啓動器
modpack.wizard.step.initialization.save=選擇要導出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啓動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應當將不允許非官方途徑傳播的 Mod、材質包等納入整合包。\n整合包會保存您目前的下載源設置
modpack.wizard.step.initialization.exported_version=要匯出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啟動器
modpack.wizard.step.initialization.save=選擇要匯出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啟動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應將不允許非官方途徑傳播的 Mod 模組、材質包等納入整合包。\n整合包會儲存您目前的下載來源設定
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.add=新增模組
mods.add.failed=新增模組 %s 失敗。
mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile=設定
profile.default=目前目錄
profile.home=主資料夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.new=建立設定
profile.title=遊戲目錄
profile.selected=已選中
profile.use_relative_path=若可能,遊戲目錄使用相對路徑
profile.selected=已選取
profile.use_relative_path=若可能,則在遊戲目錄使用相對路徑
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
selector.choose_file=選擇檔案
selector.custom=自訂
settings=普通設置
settings=普通設定
settings.advanced=高級設置
settings.advanced=進階設定
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.game_dir.default=默認(.minecraft/)
settings.advanced.game_dir.default=預設(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=啓動器默認添加的參數(請不要重複添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=內存永久保存區域(不必填寫,MB)
settings.advanced.java_args_default=啟動器預設新增的參數(請勿重複新增):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(不必填寫,格式:MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啓動後結束啓動器
settings.advanced.launcher_visibility.hide=遊戲啓動後隱藏啓動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啓動器並在遊戲結束後重新打開
settings.advanced.launcher_visibility.keep=保持啓動器可見
settings.advanced.launcher_visible=啓動器可見性
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.no_common=不使用公共路徑
settings.advanced.no_jvm_args=不添加默認的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啓動前執行命令(不必填寫,將在遊戲啓動前調用)
settings.advanced.server_ip=直入服務器 IP 地址(不必填寫,啓動遊戲後直接進入對應服務器)
settings.advanced.no_jvm_args=不新增預設的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啟動前執行命令(不必填寫,將在遊戲啟動前呼叫使用)
settings.advanced.server_ip=直接進入伺服器 IP 位址(不必填寫,啟動遊戲後直接進入對應伺服器)
settings.advanced.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
settings.custom=自訂
settings.game=遊戲設置
settings.game.dimension=遊戲窗口分辨率
settings.game=遊戲設定
settings.game.dimension=遊戲介面解析度大小
settings.game.exploration=瀏覽
settings.game.fullscreen=全屏
settings.game.fullscreen=全螢幕
settings.game.game_directory=遊戲路徑(版本隔離)
settings.game.game_directory.choose=選擇遊戲路徑
settings.game.java_directory=Java 路徑
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.management=管理
settings.icon=遊戲圖標
settings.icon=遊戲圖示
settings.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher=啟動器設定
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫檔案放於此集中管理,如果遊戲資料夾內有現成的將不會使用公共庫檔案
settings.launcher.download_source=下載來源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.log_font=記錄字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份驗證
settings.launcher.proxy.disable=使用系統代理
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.port=埠 (Port)
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.proxy.username=帳戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.max_memory=最大記憶體(MB)
settings.physical_memory=實體記憶體大小
settings.show_log=查看記錄
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
settings.type.global=全域版本設定(使用該設定的版本共用一套設定)
settings.type=版本設定類型
settings.type.special=單獨版本設定(不會影響到其他版本的設定)
update=啓動器更新
update=啟動器更新
update.channel.dev=更新到開發版
update.channel.stable=更新到推薦版本
update.checking=正在檢查更新
update.failed=更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。選擇更新到開發版導致你電腦中所有的HMCL更新至開發版
update.latest=當前版本爲最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
update.found=發現到更新
update.newest_version=最新版本為:%s
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址打開頁面
update.tooltip=更新
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.forbidden_name=此版本名稱不受支持,請換一個名字
version.game.old=遠古版
version.game.release=穩定版
version.game.snapshot=測試版
version.launch=啓動遊戲
version.launch_script=生成啓動腳本
version.launch_script.failed=生成啓動腳本失敗
version.launch_script.save=保存啓動腳本
version.launch_script.success=啓動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
version.forbidden_name=此版本名稱不受支援,請換一個名字
version.game.old=老舊版本
version.game.release=穩定版本
version.game.snapshot=測試版本
version.launch=啟動遊戲
version.launch_script=生成啟動腳本
version.launch_script.failed=生成啟動腳本失敗
version.launch_script.save=儲存啟動腳本
version.launch_script.success=啟動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源設定(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.manage.rename.fail=重命名版本失敗,可能文件被佔用或者名字有特殊字符
version.settings=遊戲設置
version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的檔案!
version.manage.remove.confirm.trash=真的要刪除版本 %s 嗎?你可以在系統的資源回收桶 (或垃圾桶) 中還原資料夾 %s 來找回該版本。
version.manage.remove.confirm.independent=由於該遊戲使用了版本隔離,所以刪除該版本將導致該遊戲的存檔等資料一同被刪除,真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫檔案
version.manage.rename=重新命名該版本
version.manage.rename.message=請輸入新名稱
version.manage.rename.fail=重新命名版本失敗,可能檔案被佔用或者名字有特殊字元
version.settings=遊戲設定
version.update=更新
wizard.prev=< 上一步
@@ -83,6 +83,7 @@ crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩
download=下载
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
@@ -92,13 +93,13 @@ extension.png=图片文件
extension.sh=Bash 脚本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对 "%s" 目录及该目录下的文件拥有读写权限。
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
folder.coremod=核心 MOD 文件夹
folder.game=游戏文件夹
folder.mod=MOD文件夹
folder.resourcepacks=资源包文件夹
@@ -132,10 +133,11 @@ install.success=安装成功
lang=简体中文
lang.default=跟随系统语言
launch.advice.different_platform=您的系统是 64 位的但是 Java 是 32 位的,推荐您安装 64 位 Java。
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java9=不高于 1.13 的安装了 Mod 的 Minecraft 不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要需要 Java 8。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.failed=启动失败
@@ -258,7 +260,7 @@ settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,单位 MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
@@ -317,7 +319,7 @@ update.checking=正在检查更新
update.failed=更新失败
update.found=发现更新
update.newest_version=最新版本为:%s
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。选择更新到开发版导致你电脑中所有的HMCL更新至开发版
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.tooltip=更新
@@ -334,7 +336,9 @@ version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
version.manage.remove.confirm.trash=真的要删除版本 %s 吗?你可以在系统的回收站中恢复文件夹 %s 来找回该版本。
version.manage.remove.confirm.independent=由于该游戏使用了版本隔离,所以删除该版本将导致该游戏的存档等数据一同被删除,真的要删除版本 %s 吗?
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要改成的名字
@@ -64,8 +64,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
return baseURL
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
@@ -28,6 +28,8 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.ParallelTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.ExceptionalFunction;
/**
* Note: This class has no state.
@@ -63,7 +65,6 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
public Task checkGameCompletionAsync(Version version) {
return new ParallelTask(
new GameAssetDownloadTask(this, version),
new GameLoggingDownloadTask(this, version),
new GameLibrariesTask(this, version)
);
}
@@ -77,7 +78,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion) {
public Task installLibraryAsync(Version version, RemoteVersion libraryVersion) {
if (libraryVersion instanceof ForgeRemoteVersion)
return new ForgeInstallTask(this, version, (ForgeRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
@@ -97,4 +98,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
throw new IllegalArgumentException("Remote library " + libraryVersion + " is unrecognized.");
}
public ExceptionalFunction<AutoTypingMap<String>, Task, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return var -> installLibraryAsync(var.get("version"), libraryVersion);
}
}
@@ -55,7 +55,6 @@ public class DefaultGameBuilder extends GameBuilder {
variables.set("version", version);
Task result = new ParallelTask(
new GameAssetDownloadTask(dependencyManager, version),
new GameLoggingDownloadTask(dependencyManager, version),
downloadGameAsync(gameVersion, version),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version)); // using [with] because download failure here are tolerant.
@@ -67,8 +66,8 @@ public class DefaultGameBuilder extends GameBuilder {
if (toolVersions.containsKey("optifine"))
result = result.then(libraryTaskHelper(gameVersion, "optifine"));
for (RemoteVersion<?> remoteVersion : remoteVersions)
result = result.then(var -> dependencyManager.installLibraryAsync(var.get("version"), remoteVersion));
for (RemoteVersion remoteVersion : remoteVersions)
result = result.then(dependencyManager.installLibraryAsync(remoteVersion));
return result;
}).finalized((variables, isDependentsSucceeded) -> {
@@ -67,7 +67,7 @@ public interface DependencyManager {
* @param libraryVersion the remote version of being installed library.
* @return the task to install the specific library.
*/
Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion);
Task installLibraryAsync(Version version, RemoteVersion libraryVersion);
/**
* Get registered version list.
@@ -31,7 +31,7 @@ public abstract class GameBuilder {
protected String name = "";
protected String gameVersion = "";
protected final Map<String, String> toolVersions = new HashMap<>();
protected final Set<RemoteVersion<?>> remoteVersions = new HashSet<>();
protected final Set<RemoteVersion> remoteVersions = new HashSet<>();
public String getName() {
return name;
@@ -64,7 +64,7 @@ public abstract class GameBuilder {
return this;
}
public GameBuilder version(RemoteVersion<?> remoteVersion) {
public GameBuilder version(RemoteVersion remoteVersion) {
remoteVersions.add(remoteVersion);
return this;
}
@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import java.util.Optional;
public final class LibraryAnalyzer {
private final Library forge;
private final Library liteLoader;
private final Library optiFine;
public LibraryAnalyzer(Library forge, Library liteLoader, Library optiFine) {
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
}
public Optional<Library> getForge() {
return Optional.ofNullable(forge);
}
public boolean hasForge() {
return forge != null;
}
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 static LibraryAnalyzer analyze(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
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;
}
return new LibraryAnalyzer(forge, liteLoader, optiFine);
}
}
@@ -17,14 +17,12 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.CommandBuilder;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
public class MaintainTask extends TaskResult<Version> {
@@ -46,55 +44,41 @@ public class MaintainTask extends TaskResult<Version> {
}
public static Version maintain(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
List<String> args = new ArrayList<>(StringUtils.tokenize(version.getMinecraftArguments().orElse("")));
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<String> mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
List<Argument> game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
boolean useMcArgs = mcArgs != null;
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge"))
forge = library;
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader"))
liteLoader = library;
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine"))
optiFine = library;
}
if (forge == null) {
removeTweakClass(args, "forge");
if (!libraryAnalyzer.hasForge()) {
removeTweakClass(useMcArgs, mcArgs, game, "forge");
}
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
if (liteLoader == null) {
removeTweakClass(args, "liteloader");
removeTweakClass(useMcArgs, mcArgs, game, "liteloader");
if (libraryAnalyzer.hasLiteLoader()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
removeTweakClass(useMcArgs, mcArgs, game, "optifine");
if (libraryAnalyzer.hasOptiFine()) {
if (!libraryAnalyzer.hasLiteLoader() && !libraryAnalyzer.hasForge()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineTweaker");
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineForgeTweaker");
}
}
Version result;
if (useMcArgs) {
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
// so we regenerate the minecraftArgument without escaping.
result = version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString());
} else {
if (!StringUtils.containsOne(args, "LiteLoaderTweaker")) {
args.add("--tweakClass");
args.add("com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
result = version.setArguments(version.getArguments().map(args -> args.withGame(game)).orElse(new Arguments(game, Collections.emptyList())));
}
if (optiFine == null) {
removeTweakClass(args, "optifine");
}
if (liteLoader == null && forge == null && optiFine != null) {
if (!StringUtils.containsOne(args, "optifine.OptiFineTweaker")) {
args.add("--tweakClass");
args.add("optifine.OptiFineTweaker");
}
}
if ((liteLoader != null || forge != null) && optiFine != null) {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
removeTweakClass(args, "optifine");
args.add("--tweakClass");
args.add("optifine.OptiFineForgeTweaker");
}
return version.setMinecraftArguments(new CommandBuilder().addAll(args).toString());
return result;
}
@Override
@@ -102,17 +86,43 @@ public class MaintainTask extends TaskResult<Version> {
return id;
}
private static void removeTweakClass(List<String> args, String target) {
for (int i = 0; i < args.size(); ++i) {
if (args.get(i).toLowerCase().contains(target)) {
if (i > 0 && args.get(i - 1).equals("--tweakClass")) {
args.remove(i - 1);
args.remove(i - 1);
i -= 2;
private static void removeTweakClass(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String target) {
if (useMcArgs) {
for (int i = 0; i + 1 < mcArgs.size(); ++i) {
String arg0Str = mcArgs.get(i);
String arg1Str = mcArgs.get(i + 1);
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
mcArgs.remove(i);
mcArgs.remove(i);
--i;
}
}
} else {
for (int i = 0; i + 1 < game.size(); ++i) {
Argument arg0 = game.get(i);
Argument arg1 = game.get(i + 1);
if (arg0 instanceof StringArgument && arg1 instanceof StringArgument) {
// We need to preserve the tokens
String arg0Str = arg0.toString();
String arg1Str = arg1.toString();
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
game.remove(i);
game.remove(i);
--i;
}
}
}
}
}
private static void addArgument(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String... args) {
if (useMcArgs) {
mcArgs.addAll(Arrays.asList(args));
} else {
for (String arg : args)
game.add(new StringArgument(arg));
}
}
public static final String ID = "version";
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
* @see <a href="http://wiki.vg">http://wiki,vg</a>
@@ -48,7 +48,7 @@ public class MojangDownloadProvider implements DownloadProvider {
case "liteloader":
return LiteLoaderVersionList.INSTANCE;
case "optifine":
return OptiFineVersionList.INSTANCE;
return OptiFineBMCLVersionList.INSTANCE;
default:
throw new IllegalArgumentException("Unrecognized version list id: " + id);
}
@@ -27,26 +27,36 @@ import java.util.Objects;
*
* @author huangyuhui
*/
public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
public class RemoteVersion implements Comparable<RemoteVersion> {
private final String gameVersion;
private final String selfVersion;
private final String url;
private final T tag;
private final Type type;
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param tag some necessary information for Installer Task.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, T tag) {
public RemoteVersion(String gameVersion, String selfVersion, String url) {
this(gameVersion, selfVersion, url, Type.UNCATEGORIZED);
}
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, Type type) {
this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
this.url = Objects.requireNonNull(url);
this.tag = tag;
this.type = Objects.requireNonNull(type);
}
public String getGameVersion() {
@@ -57,14 +67,14 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return selfVersion;
}
public T getTag() {
return tag;
}
public String getUrl() {
return url;
}
public Type getVersionType() {
return type;
}
@Override
public boolean equals(Object obj) {
return obj instanceof RemoteVersion && Objects.equals(selfVersion, ((RemoteVersion) obj).selfVersion);
@@ -80,13 +90,19 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return new ToStringBuilder(this)
.append("selfVersion", selfVersion)
.append("gameVersion", gameVersion)
.append("tag", tag)
.toString();
}
@Override
public int compareTo(RemoteVersion<T> o) {
public int compareTo(RemoteVersion o) {
// newer versions are smaller than older versions
return VersionNumber.asVersion(o.selfVersion).compareTo(VersionNumber.asVersion(selfVersion));
}
public enum Type {
UNCATEGORIZED,
RELEASE,
SNAPSHOT,
OLD
}
}
@@ -26,18 +26,18 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The remote version list.
*
* @param <T> The type of {@code RemoteVersion<T>}, the type of tags.
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
*
* @author huangyuhui
*/
public abstract class VersionList<T> {
public abstract class VersionList<T extends RemoteVersion> {
/**
* the remote version list.
* key: game version.
* values: corresponding remote versions.
*/
protected final SimpleMultimap<String, RemoteVersion<T>> versions = new SimpleMultimap<String, RemoteVersion<T>>(HashMap::new, TreeSet::new);
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
/**
* True if the version list has been loaded.
@@ -46,6 +46,8 @@ public abstract class VersionList<T> {
return !versions.isEmpty();
}
public abstract boolean hasType();
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
@@ -68,11 +70,10 @@ public abstract class VersionList<T> {
});
}
private Collection<RemoteVersion<T>> getVersionsImpl(String gameVersion) {
protected Collection<T> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
Collection<RemoteVersion<T>> ans = versions.get(gameVersion);
return ans.isEmpty() ? versions.values() : ans;
return versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
@@ -84,7 +85,7 @@ public abstract class VersionList<T> {
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
public final Collection<RemoteVersion<T>> getVersions(String gameVersion) {
public final Collection<T> getVersions(String gameVersion) {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion));
}
@@ -95,11 +96,11 @@ public abstract class VersionList<T> {
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
public final Optional<RemoteVersion<T>> getVersion(String gameVersion, String remoteVersion) {
public final Optional<T> getVersion(String gameVersion, String remoteVersion) {
lock.readLock().lock();
try {
RemoteVersion<T> result = null;
for (RemoteVersion<T> it : versions.get(gameVersion))
T result = null;
for (T it : versions.get(gameVersion))
if (remoteVersion.equals(it.getSelfVersion()))
result = it;
return Optional.ofNullable(result);
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.RemoteVersion;
public class ForgeRemoteVersion extends RemoteVersion<Void> {
public class ForgeRemoteVersion extends RemoteVersion {
/**
* Constructor.
*
@@ -28,6 +28,6 @@ public class ForgeRemoteVersion extends RemoteVersion<Void> {
* @param url the installer or universal jar URL.
*/
public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) {
super(gameVersion, selfVersion, url, null);
super(gameVersion, selfVersion, url);
}
}
@@ -32,13 +32,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class ForgeVersionList extends VersionList<Void> {
public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
public static final ForgeVersionList INSTANCE = new ForgeVersionList();
private ForgeVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST)));
@@ -75,8 +75,6 @@ public final class GameAssetDownloadTask extends Task {
@Override
public void execute() throws Exception {
int size = refreshTask.getResult().size();
int downloaded = 0;
for (Map.Entry<File, AssetObject> entry : refreshTask.getResult()) {
if (Thread.interrupted())
throw new InterruptedException();
@@ -88,29 +86,11 @@ public final class GameAssetDownloadTask extends Task {
Logging.LOG.log(Level.SEVERE, "Unable to create new file " + file + ", because parent directory cannot be created");
continue;
}
if (file.isDirectory())
if (file.isDirectory() || file.isFile() && file.exists())
continue;
boolean flag = true;
try {
// check the checksum of file to ensure that the file is not need to re-download.
if (file.exists()) {
String sha1 = encodeHex(digest("SHA-1", FileUtils.readBytes(file)));
if (sha1.equals(assetObject.getHash())) {
++downloaded;
Logging.LOG.finest("File $file has been downloaded successfully, skipped downloading");
updateProgress(downloaded, size);
continue;
}
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to get hash code of file " + file, e);
flag = !file.exists();
}
if (flag) {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.game.DownloadType;
import org.jackhuang.hmcl.game.LoggingInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
/**
* This task is to download log4j configuration file provided in minecraft.json.
*
* @author huangyuhui
*/
public final class GameLoggingDownloadTask extends Task {
private final DependencyManager dependencyManager;
private final Version version;
private final List<Task> dependencies = new LinkedList<>();
/**
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
*/
public GameLoggingDownloadTask(DependencyManager dependencyManager, Version version) {
this.dependencyManager = dependencyManager;
this.version = version;
setSignificance(TaskSignificance.MODERATE);
}
@Override
public Collection<Task> getDependencies() {
return dependencies;
}
@Override
public void execute() {
if (version.getLogging() == null || !version.getLogging().containsKey(DownloadType.CLIENT))
return;
LoggingInfo logging = version.getLogging().get(DownloadType.CLIENT);
File file = dependencyManager.getGameRepository().getLoggingObject(version.getId(), version.getAssetIndex().getId(), logging);
if (!file.exists())
dependencies.add(new FileDownloadTask(NetworkUtils.toURL(logging.getFile().getUrl()), file));
}
}
@@ -17,12 +17,9 @@
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
@@ -30,64 +27,45 @@ import java.util.Date;
*
* @author huangyuhui
*/
public final class GameRemoteVersion implements Validation {
@Immutable
public final class GameRemoteVersion extends RemoteVersion {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final ReleaseType type;
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersion() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) {
super(gameVersion, selfVersion, url, getReleaseType(type));
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
this.time = time;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion))
return 0;
return ((GameRemoteVersion) o).getTime().compareTo(getTime());
}
private static Type getReleaseType(ReleaseType type) {
if (type == null) return Type.UNCATEGORIZED;
switch (type) {
case RELEASE:
return Type.RELEASE;
case SNAPSHOT:
return Type.SNAPSHOT;
case UNKNOWN:
return Type.UNCATEGORIZED;
default:
return Type.OLD;
}
}
}
@@ -0,0 +1,93 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import java.util.Date;
/**
*
* @author huangyuhui
*/
public final class GameRemoteVersionInfo implements Validation {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersionInfo() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
}
}
@@ -1,55 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
/**
*
* @author huangyuhui
*/
@Immutable
public final class GameRemoteVersionTag {
private final ReleaseType type;
private final Date time;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public GameRemoteVersionTag() {
this(ReleaseType.UNKNOWN, new Date());
}
public GameRemoteVersionTag(ReleaseType type, Date time) {
this.type = type;
this.time = time;
}
public Date getTime() {
return time;
}
public ReleaseType getType() {
return type;
}
}
@@ -31,7 +31,7 @@ import java.util.List;
public final class GameRemoteVersions {
@SerializedName("versions")
private final List<GameRemoteVersion> versions;
private final List<GameRemoteVersionInfo> versions;
@SerializedName("latest")
private final GameRemoteLatestVersions latest;
@@ -44,7 +44,7 @@ public final class GameRemoteVersions {
this(Collections.emptyList(), null);
}
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {
public GameRemoteVersions(List<GameRemoteVersionInfo> versions, GameRemoteLatestVersions latest) {
this.versions = versions;
this.latest = latest;
}
@@ -53,7 +53,7 @@ public final class GameRemoteVersions {
return latest;
}
public List<GameRemoteVersion> getVersions() {
public List<GameRemoteVersionInfo> getVersions() {
return versions;
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collection;
import java.util.Collections;
@@ -32,13 +33,28 @@ import java.util.Collections;
*
* @author huangyuhui
*/
public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
public final class GameVersionList extends VersionList<GameRemoteVersion> {
public static final GameVersionList INSTANCE = new GameVersionList();
private GameVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
protected Collection<GameRemoteVersion> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
return StringUtils.isBlank(gameVersion) ? versions.values() : versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.getVersionListURL()));
@@ -56,12 +72,12 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
versions.clear();
GameRemoteVersions root = Constants.GSON.fromJson(task.getResult(), GameRemoteVersions.class);
for (GameRemoteVersion remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new RemoteVersionGame(
for (GameRemoteVersionInfo remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new GameRemoteVersion(
remoteVersion.getGameVersion(),
remoteVersion.getGameVersion(),
remoteVersion.getUrl(),
new GameRemoteVersionTag(remoteVersion.getType(), remoteVersion.getReleaseTime()))
remoteVersion.getType(), remoteVersion.getReleaseTime())
);
}
} finally {
@@ -70,15 +86,4 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
}
};
}
private static class RemoteVersionGame extends RemoteVersion<GameRemoteVersionTag> {
public RemoteVersionGame(String gameVersion, String selfVersion, String url, GameRemoteVersionTag tag) {
super(gameVersion, selfVersion, url, tag);
}
@Override
public int compareTo(RemoteVersion<GameRemoteVersionTag> o) {
return o.getTag().getTime().compareTo(getTag().getTime());
}
}
}
@@ -12,7 +12,10 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
@@ -23,15 +26,15 @@ import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class LibraryDownloadTask extends Task {
private final FileDownloadTask xzTask;
private final FileDownloadTask task;
private final LibraryDownloadTaskHelper helperTask;
private final File jar;
private final File xzFile;
private final Library library;
private boolean downloaded = false;
private boolean xz;
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
setSignificance(TaskSignificance.MODERATE);
if (library.is("net.minecraftforge", "forge"))
library = library.setClassifier("universal");
@@ -42,20 +45,12 @@ public final class LibraryDownloadTask extends Task {
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
xzFile, null, 1);
xzTask.setSignificance(TaskSignificance.MINOR);
setSignificance(TaskSignificance.MODERATE);
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
helperTask = new LibraryDownloadTaskHelper(file, url);
}
@Override
public Collection<? extends Task> getDependents() {
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
return Collections.singleton(helperTask);
}
@Override
@@ -66,32 +61,75 @@ public final class LibraryDownloadTask extends Task {
@Override
public void execute() throws Exception {
if (!isDependentsSucceeded()) {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
if (helperTask.task == null) {
// NetworkUtils.URLExists failed.
throw new LibraryDownloadException(library, helperTask.getLastException());
} else {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = helperTask.task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
}
} else {
if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums())) {
jar.delete();
throw new IOException("Checksum failed for " + library);
}
}
}
private class LibraryDownloadTaskHelper extends Task {
private FileDownloadTask task;
private final File file;
private final String url;
public LibraryDownloadTaskHelper(File file, String url) {
this.file = file;
this.url = url;
setName(library.getName());
}
if (library.getChecksums() != null) {
unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums()))
throw new IOException("Checksum failed for " + library);
@Override
public boolean isRelyingOnDependencies() {
return true;
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
if (NetworkUtils.URLExists(packXz)) {
task = new FileDownloadTask(packXz, xzFile, null);
xz = true;
} else {
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
xz = false;
}
}
}
private static boolean checksumValid(File libPath, List<String> checksums) {
try {
if ((checksums == null) || (checksums.isEmpty())) {
if (checksums == null || checksums.isEmpty()) {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums);
}
return validateJar(fileData, checksums);
return valid;
} catch (IOException e) {
e.printStackTrace();
}
@@ -104,7 +142,7 @@ public final class LibraryDownloadTask extends Task {
JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data));
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
byte[] eData = IOUtils.readFullyAsByteArray(jar);
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
if (entry.getName().equals("checksums.sha1")) {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
}
@@ -115,8 +153,8 @@ public final class LibraryDownloadTask extends Task {
}
jar.close();
if (hashes != null) {
boolean passed = !checksums.contains(files.get("checksums.sha1"));
if (passed) {
boolean failed = !checksums.contains(files.get("checksums.sha1"));
if (!failed) {
for (String hash : hashes) {
if ((!hash.trim().equals("")) && (hash.contains(" "))) {
String[] e = hash.split(" ");
@@ -125,17 +163,17 @@ public final class LibraryDownloadTask extends Task {
String checksum = files.get(target);
if ((!files.containsKey(target)) || (checksum == null)) {
Logging.LOG.warning(" " + target + " : missing");
passed = false;
failed = true;
break;
} else if (!checksum.equals(validChecksum)) {
Logging.LOG.warning(" " + target + " : failed (" + checksum + ", " + validChecksum + ")");
passed = false;
failed = true;
break;
}
}
}
}
return passed;
return !failed;
}
return false;
}
@@ -62,7 +62,7 @@ public final class VersionJsonDownloadTask extends Task {
@Override
public void execute() {
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), ID));
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderBMCLVersionList INSTANCE = new LiteLoaderBMCLVersionList();
private LiteLoaderBMCLVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
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.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
@@ -74,18 +71,12 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
);
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library)));
String mcArg = version.getMinecraftArguments().orElse("");
if (mcArg.contains("--tweakClass optifine.OptiFineTweaker"))
mcArg = mcArg.replace("--tweakClass optifine.OptiFineTweaker", "");
Version tempVersion = version.setLibraries(Lang.merge(remote.getLibraries(), Collections.singleton(library)));
setResult(version
.setMainClass("net.minecraft.launchwrapper.Launch")
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
.setLogging(Collections.emptyMap())
.setMinecraftArguments(mcArg + " --tweakClass " + remote.getTag().getTweakClass())
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
);
dependencies.add(new GameLibrariesTask(dependencyManager, tempVersion));
@@ -22,7 +22,9 @@ import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersionTag> {
public class LiteLoaderRemoteVersion extends RemoteVersion {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* Constructor.
*
@@ -31,6 +33,18 @@ public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersi
* @param url the installer or universal jar URL.
*/
LiteLoaderRemoteVersion(String gameVersion, String selfVersion, String url, String tweakClass, Collection<Library> libraries) {
super(gameVersion, selfVersion, url, new LiteLoaderRemoteVersionTag(tweakClass, libraries));
super(gameVersion, selfVersion, url);
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -1,54 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
import java.util.Collections;
/**
*
* @author huangyuhui
*/
public final class LiteLoaderRemoteVersionTag {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public LiteLoaderRemoteVersionTag() {
this("", Collections.emptySet());
}
public LiteLoaderRemoteVersionTag(String tweakClass, Collection<Library> libraries) {
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderVersionList INSTANCE = new LiteLoaderVersionList();
private LiteLoaderVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -33,13 +33,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class OptiFineBMCLVersionList extends VersionList<Void> {
public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVersion> {
public static final OptiFineBMCLVersionList INSTANCE = new OptiFineBMCLVersionList();
private OptiFineBMCLVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi2.bangbang93.com/optifine/versionlist"));
@@ -57,16 +62,16 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
if (element.getType() == null || "pre".equals(element.getPatch()))
continue;
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;
boolean isPre = element.getPatch() != null && (element.getPatch().equals("pre") || element.getPatch().startsWith("alpha"));
if (StringUtils.isBlank(element.getGameVersion()))
continue;
VersionNumber.parseVersion(element.getGameVersion())
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror)));
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre)));
}
}
};
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -70,46 +69,24 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
@Override
public void execute() {
String remoteVersion = remote.getGameVersion() + "-" + remote.getSelfVersion();
String remoteVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
Library library = new Library(
"net.optifine", "optifine", remoteVersion, null, null,
"optifine", "OptiFine", remoteVersion, null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"net/optifine/optifine/" + remoteVersion + "/optifine-" + remoteVersion + ".jar",
"optifine/OptiFine/" + remoteVersion + "/OptiFine-" + remoteVersion + ".jar",
remote.getUrl()))
);
List<Library> libraries = new LinkedList<>();
libraries.add(library);
boolean hasFMLTweaker = false;
if (version.getMinecraftArguments().isPresent() && version.getMinecraftArguments().get().contains("FMLTweaker"))
hasFMLTweaker = true;
if (version.getArguments().isPresent()) {
List<Argument> game = version.getArguments().get().getGame();
if (game.stream().anyMatch(arg -> arg.toString(Collections.emptyMap(), Collections.emptyMap()).contains("FMLTweaker")))
hasFMLTweaker = true;
}
/*Arguments arguments = Lang.get(version.getArguments());
if (!hasFMLTweaker)
arguments = Arguments.addGameArguments(arguments, "--tweakClass", "optifine.OptiFineTweaker");
*/
String minecraftArguments = version.getMinecraftArguments().orElse("");
if (!hasFMLTweaker)
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineTweaker";
else
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineForgeTweaker";
if (version.getMainClass() == null || !version.getMainClass().startsWith("net.minecraft.launchwrapper."))
libraries.add(0, new Library("net.minecraft", "launchwrapper", "1.12"));
setResult(version
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
.setMinecraftArguments(minecraftArguments)
//.setArguments(arguments)
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
);
dependencies.add(new GameLibrariesTask(dependencyManager, version.setLibraries(libraries)));
@@ -21,11 +21,11 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import java.util.function.Supplier;
public class OptiFineRemoteVersion extends RemoteVersion<Void> {
public class OptiFineRemoteVersion extends RemoteVersion {
private final Supplier<String> url;
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url) {
super(gameVersion, selfVersion, "", null);
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url, boolean snapshot) {
super(gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE);
this.url = url;
}
@@ -1,121 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.optifine;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author huangyuhui
*/
public final class OptiFineVersionList extends VersionList<Void> {
private static final Pattern PATTERN = Pattern.compile("OptiFine (.*?) ");
private static final Pattern LINK_PATTERN = Pattern.compile("\"downloadx\\?f=OptiFine(.*)\"");
public static final OptiFineVersionList INSTANCE = new OptiFineVersionList();
private OptiFineVersionList() {
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("https://optifine.net/downloads"));
return new Task() {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
lock.writeLock().lock();
try {
versions.clear();
String html = task.getResult().replace("&nbsp;", " ").replace("&gt;", ">").replace("&lt;", "<").replace("<br>", "<br />");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder db = factory.newDocumentBuilder();
Document doc = db.parse(new ByteArrayInputStream(html.getBytes("UTF-8")));
Element r = doc.getDocumentElement();
NodeList tables = r.getElementsByTagName("table");
for (int i = 0; i < tables.getLength(); i++) {
Element e = (Element) tables.item(i);
if ("downloadTable".equals(e.getAttribute("class"))) {
NodeList tr = e.getElementsByTagName("tr");
for (int k = 0; k < tr.getLength(); k++) {
NodeList downloadLine = ((Element) tr.item(k)).getElementsByTagName("td");
String url = null, version = null, gameVersion = null;
for (int j = 0; j < downloadLine.getLength(); j++) {
Element td = (Element) downloadLine.item(j);
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineMirror"))
url = ((Element) td.getElementsByTagName("a").item(0)).getAttribute("href");
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
version = td.getTextContent();
}
if (version == null || url == null)
continue;
Matcher matcher = PATTERN.matcher(version);
while (matcher.find())
gameVersion = matcher.group(1);
if (gameVersion == null)
continue;
String finalURL = url;
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Lang.hideException(() -> getLink(finalURL))));
}
}
}
} finally {
lock.writeLock().unlock();
}
}
};
}
private static String getLink(String url) throws IOException {
String result = null;
String content = NetworkUtils.doGet(NetworkUtils.toURL(url));
Matcher m = LINK_PATTERN.matcher(content);
while (m.find())
result = m.group(1);
if (result == null)
throw new IllegalStateException("Cannot find version in " + content);
return "https://optifine.net/downloadx?f=OptiFine" + result;
}
}
@@ -50,10 +50,18 @@ public final class Arguments {
return game == null ? Collections.emptyList() : Collections.unmodifiableList(game);
}
public Arguments withGame(List<Argument> game) {
return new Arguments(game, jvm);
}
public List<Argument> getJvm() {
return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm);
}
public Arguments withJvm(List<Argument> jvm) {
return new Arguments(game, jvm);
}
public Arguments addGameArguments(String... gameArguments) {
return addGameArguments(Arrays.asList(gameArguments));
}
@@ -82,7 +90,7 @@ public final class Arguments {
}
public static List<String> parseStringArguments(List<String> arguments, Map<String, String> keys) {
return arguments.stream().map(str -> keys.getOrDefault(str, str)).collect(Collectors.toList());
return arguments.stream().flatMap(str -> new StringArgument(str).toString(keys, Collections.emptyMap()).stream()).collect(Collectors.toList());
}
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys) {
@@ -32,7 +32,7 @@ public class ClassicVersion extends Version {
super(true, "Classic", "${auth_player_name} ${auth_session} --workDir ${game_directory}",
null, "net.minecraft.client.Minecraft", null, null, null, null,
Arrays.asList(new ClassicLibrary("lwjgl"), new ClassicLibrary("jinput"), new ClassicLibrary("lwjgl_util")),
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0);
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0, false);
}
private static class ClassicLibrary extends Library {
@@ -160,14 +160,18 @@ public class DefaultGameRepository implements GameRepository {
if (!file.renameTo(removedFile))
return false;
versions.remove(id);
if (FileUtils.isMovingToTrashSupported()) {
return FileUtils.moveToTrash(removedFile);
}
// 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())
Logging.LOG.warning("Unable to delete file " + f);
});
versions.remove(id);
// remove the version from version list regardless of whether the directory was removed successfully or not.
try {
FileUtils.deleteDirectory(removedFile);
@@ -223,6 +223,7 @@ public class Library implements Comparable<Library> {
obj.addProperty("name", src.getName());
obj.addProperty("url", src.url);
obj.add("downloads", context.serialize(src.downloads));
obj.add("checksums", context.serialize(src.getChecksums()));
obj.add("extract", context.serialize(src.extract));
obj.add("natives", context.serialize(src.natives, new TypeToken<Map<OperatingSystem, String>>() {
}.getType()));
@@ -65,7 +65,7 @@ public class RuledArgument implements Argument {
@Override
public List<String> toString(Map<String, String> keys, Map<String, Boolean> features) {
if (CompatibilityRule.appliesToCurrentEnvironment(rules) && value != null)
if (CompatibilityRule.appliesToCurrentEnvironment(rules, features) && value != null)
return value.stream()
.map(StringArgument::new)
.map(str -> str.toString(keys, features).get(0))
@@ -54,4 +54,8 @@ public final class StringArgument implements Argument {
return Collections.singletonList(res);
}
@Override
public String toString() {
return argument;
}
}
@@ -46,10 +46,11 @@ public class Version implements Comparable<Version>, Validation {
private final Date time;
private final Date releaseTime;
private final int minimumLauncherVersion;
private final boolean hidden;
private transient final boolean resolved;
public Version(boolean resolved, String id, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, int minimumLauncherVersion) {
public Version(boolean resolved, String id, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, int minimumLauncherVersion, boolean hidden) {
this.resolved = resolved;
this.id = id;
this.minecraftArguments = minecraftArguments;
@@ -67,6 +68,7 @@ public class Version implements Comparable<Version>, Validation {
this.time = time == null ? new Date() : (Date) time.clone();
this.releaseTime = releaseTime == null ? new Date() : (Date) releaseTime.clone();
this.minimumLauncherVersion = minimumLauncherVersion;
this.hidden = hidden;
}
public Optional<String> getMinecraftArguments() {
@@ -109,6 +111,10 @@ public class Version implements Comparable<Version>, Validation {
return minimumLauncherVersion;
}
public boolean isHidden() {
return hidden;
}
public Map<DownloadType, LoggingInfo> getLogging() {
return logging == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(logging);
}
@@ -139,10 +145,6 @@ public class Version implements Comparable<Version>, Validation {
return CompatibilityRule.appliesToCurrentEnvironment(compatibilityRules);
}
public boolean install(String id) {
return false;
}
/**
* Resolve given version
*/
@@ -180,43 +182,44 @@ public class Version implements Comparable<Version>, Validation {
type,
time,
releaseTime,
Math.max(minimumLauncherVersion, parent.minimumLauncherVersion));
Math.max(minimumLauncherVersion, parent.minimumLauncherVersion),
hidden);
}
private Version setResolved() {
return new Version(true, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(true, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setId(String id) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setMinecraftArguments(String minecraftArguments) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setArguments(Arguments arguments) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setMainClass(String mainClass) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setInheritsFrom(String inheritsFrom) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setJar(String jar) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setLibraries(List<Library> libraries) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setLogging(Map<DownloadType, LoggingInfo> logging) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
@Override
@@ -47,7 +47,7 @@ public class DefaultLauncher extends Launcher {
super(repository, versionId, authInfo, options, listener, daemon);
}
private CommandBuilder generateCommandLine(File nativeFolder, boolean enableLoggingInfo) throws IOException {
private CommandBuilder generateCommandLine(File nativeFolder) throws IOException {
CommandBuilder res = new CommandBuilder();
// Executable
@@ -70,16 +70,6 @@ public class DefaultLauncher extends Launcher {
res.add("-Xdock:icon=" + repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns").getAbsolutePath());
}
Map<DownloadType, LoggingInfo> logging = version.getLogging();
if (logging != null && enableLoggingInfo) {
LoggingInfo loggingInfo = logging.get(DownloadType.CLIENT);
if (loggingInfo != null) {
File loggingFile = repository.getLoggingObject(version.getId(), version.getAssetIndex().getId(), loggingInfo);
if (loggingFile.exists())
res.add(loggingInfo.getArgument().replace("${path}", loggingFile.getAbsolutePath()));
}
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
res.add("-Duser.home=" + options.getGameDir().getParent());
@@ -153,14 +143,6 @@ public class DefaultLauncher extends Launcher {
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(LinkedList::new), configuration));
// Optional Minecraft arguments
if (options.getHeight() != null && options.getHeight() != 0 && options.getWidth() != null && options.getWidth() != 0) {
res.add("--height");
res.add(options.getHeight().toString());
res.add("--width");
res.add(options.getWidth().toString());
}
if (StringUtils.isNotBlank(options.getServerIp())) {
String[] args = options.getServerIp().split(":");
res.add("--server");
@@ -249,7 +231,9 @@ public class DefaultLauncher extends Launcher {
pair("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
pair("${user_type}", "mojang"),
pair("${assets_index_name}", version.getAssetIndex().getId()),
pair("${user_properties}", authInfo.getUserProperties())
pair("${user_properties}", authInfo.getUserProperties()),
pair("${resolution_width}", options.getWidth().toString()),
pair("${resolution_height}", options.getHeight().toString())
);
}
@@ -258,7 +242,7 @@ public class DefaultLauncher extends Launcher {
File nativeFolder = Files.createTempDirectory("minecraft").toFile();
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder, isEnablingLoggingInfo()).asList();
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
decompressNatives(nativeFolder);
@@ -310,50 +294,17 @@ public class DefaultLauncher extends Launcher {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder, false).toString());
writer.write(generateCommandLine(nativeFolder).toString());
}
if (!scriptFile.setExecutable(true))
throw new PermissionException();
}
protected boolean isEnablingLoggingInfo() {
return version.getLogging() != null && version.getLogging().containsKey(DownloadType.CLIENT)
&& !"net.minecraft.launchwrapper.Launch".equals(version.getMainClass());
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener) {
startMonitors(managedProcess, processListener, true);
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
if (isEnablingLoggingInfo())
startMonitorsWithLoggingInfo(managedProcess, processListener, isDaemon);
else
startMonitorsWithoutLoggingInfo(managedProcess, processListener, isDaemon);
}
private void startMonitorsWithLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
processListener.setProcess(managedProcess);
Log4jHandler logHandler = new Log4jHandler((line, level) -> {
processListener.onLog(line, level);
managedProcess.addLine(line);
});
logHandler.start();
managedProcess.addRelatedThread(logHandler);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), logHandler::newLine), "stdout-pump", isDaemon);
managedProcess.addRelatedThread(stdout);
Thread stderr = Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream(), it -> {
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Log4jLevel.ERROR);
managedProcess.addLine(it);
}), "stderr-pump", isDaemon);
managedProcess.addRelatedThread(stderr);
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), (exitCode, exitType) -> {
logHandler.onStopped();
processListener.onExit(exitCode, exitType);
}), "exit-waiter", isDaemon));
}
private void startMonitorsWithoutLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO));
@@ -1,180 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.launch;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.logging.Level;
/**
* This class is to parse log4j classic XML layout logging,
* since only vanilla Minecraft will enable this layout.
*
* Also supports plain logs.
*
* @author huangyuhui
*/
final class Log4jHandler extends Thread {
private final XMLReader reader;
private final BiConsumer<String, Log4jLevel> callback;
private final PipedOutputStream outputStream = new PipedOutputStream();
private final PipedInputStream inputStream = Lang.invoke(() -> new PipedInputStream(outputStream));
private final AtomicBoolean interrupted = new AtomicBoolean(false);
private final List<String> logs = new LinkedList<>();
private boolean broken = false;
public Log4jHandler(BiConsumer<String, Log4jLevel> callback) {
this.callback = callback;
newLine("<output>");
reader = Lang.invoke((ExceptionalSupplier<XMLReader, SAXException>) XMLReaderFactory::createXMLReader);
reader.setContentHandler(new Log4jHandlerImpl());
}
@Override
public void run() {
setName("log4j-handler");
try {
InputSource inputSource = new InputSource(inputStream);
reader.parse(inputSource);
} catch (InterruptedIOException e) {
// Game has been interrupted.
interrupted.set(true);
} catch (SAXException | IOException e) {
Logging.LOG.log(Level.WARNING, "An error occurred when reading console lines", e);
}
}
public void onStopped() {
if (interrupted.get())
return;
Lang.invoke(() -> Schedulers.newThread().schedule(() -> {
if (!interrupted.get()) {
newLine("</output>").get();
outputStream.close();
join();
}
}).get());
}
public Future<?> newLine(String log) {
return Schedulers.computation().schedule(() -> {
try {
// Prevent from namespace parsing
String line = log.replace("log4j:", "log4j_");
logs.add(line);
if (broken)
System.out.println(line);
byte[] bytes = (line + OperatingSystem.LINE_SEPARATOR).getBytes(Constants.SYSTEM_CHARSET);
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
// Ignoring IOException, including read end dead.
if (!broken) {
Logging.LOG.log(Level.WARNING, "An error occurred when writing console lines", e);
logs.forEach(System.out::println);
broken = true;
} else {
// Output plain XML to user
callback.accept(log, Log4jLevel.INFO);
}
}
});
}
private class Log4jHandlerImpl extends DefaultHandler {
private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
private String date = "", thread = "", logger = "";
private StringBuilder message;
private Log4jLevel level;
private boolean readingMessage = false;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (localName) {
case "log4j_Event":
message = new StringBuilder();
Date d = new Date(Long.parseLong(attributes.getValue("timestamp")));
date = format.format(d);
try {
level = Log4jLevel.valueOf(attributes.getValue("level"));
} catch (IllegalArgumentException e) {
level = Log4jLevel.INFO;
}
thread = attributes.getValue("thread");
logger = attributes.getValue("logger");
if ("STDERR".equals(logger))
level = Log4jLevel.ERROR;
break;
case "log4j_Message":
readingMessage = true;
break;
case "log4j_Throwable":
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
switch (localName) {
case "log4j_Event":
callback.accept("[" + date + "] [" + thread + "/" + level.name() + "] [" + logger + "] " + message.toString(), level);
break;
case "log4j_Message":
readingMessage = false;
break;
}
}
@Override
public void characters(char[] ch, int start, int length) {
String line = new String(ch, start, length);
if (line.trim().isEmpty())
return;
if (readingMessage)
message.append(line).append(OperatingSystem.LINE_SEPARATOR);
else
callback.accept(line, Optional.ofNullable(Log4jLevel.guessLevel(line)).orElse(Log4jLevel.INFO));
}
}
}
@@ -46,7 +46,7 @@ public final class ModInfo implements Comparable<ModInfo> {
}
public ModInfo(File file, String name, String description) {
this(file, name, description, "unknown", "unknown", "unknown", "");
this(file, name, description, "", "", "", "");
}
public ModInfo(File file, String name, String description, String authors, String version, String gameVersion, String url) {
@@ -108,7 +108,7 @@ public abstract class Task {
return true;
}
private String name = getClass().toString();
private String name = getClass().getName();
public String getName() {
return name;
@@ -20,8 +20,10 @@ package org.jackhuang.hmcl.util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@@ -95,6 +97,30 @@ public final class FileUtils {
return Lang.test(() -> deleteDirectory(directory));
}
public static boolean moveToTrash(File file) {
try {
java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
Method moveToTrash = desktop.getClass().getMethod("moveToTrash", File.class);
moveToTrash.invoke(desktop, file);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Check if {@code java.awt.Desktop.moveToTrash} exists.
* @return true if the method exists.
*/
public static boolean isMovingToTrashSupported() {
try {
java.awt.Desktop.class.getMethod("moveToTrash", File.class);
return true;
} catch (ReflectiveOperationException e) {
return false;
}
}
public static void cleanDirectory(File directory)
throws IOException {
if (!directory.exists()) {
@@ -31,6 +31,12 @@ public final class IOUtils {
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
copyTo(stream, result);
return result.toByteArray();
}
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
try (InputStream is = stream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleBooleanProperty instead
*/
@Deprecated
public class ImmediateBooleanProperty extends SimpleBooleanProperty {
@Override
@@ -48,31 +46,9 @@ public class ImmediateBooleanProperty extends SimpleBooleanProperty {
super.unbind();
}
private Consumer<Boolean> consumer = null;
private ChangeListener<Boolean> listener = null;
public void setChangedListener(Consumer<Boolean> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Boolean> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Boolean> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateBooleanProperty(Object bean, String name, boolean initialValue) {
super(bean, name, initialValue);
ChangeListener<Boolean> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue);
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -1,81 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
*/
public class ImmediateDoubleProperty extends SimpleDoubleProperty {
@Override
public void set(double newValue) {
super.get();
super.set(newValue);
}
@Override
public void bind(ObservableValue<? extends Number> newObservable) {
super.get();
super.bind(newObservable);
}
@Override
public void unbind() {
super.get();
super.unbind();
}
private Consumer<Double> consumer = null;
private ChangeListener<Number> listener = null;
public void setChangedListener(Consumer<Double> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Number> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Double> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateDoubleProperty(Object bean, String name, double initialValue) {
super(bean, name, initialValue);
ChangeListener<Number> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue.doubleValue());
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
}
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleIntegerProperty instead
*/
@Deprecated
public class ImmediateIntegerProperty extends SimpleIntegerProperty {
@Override
@@ -48,31 +46,9 @@ public class ImmediateIntegerProperty extends SimpleIntegerProperty {
super.unbind();
}
private Consumer<Integer> consumer = null;
private ChangeListener<Number> listener = null;
public void setChangedListener(Consumer<Integer> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Number> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Integer> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateIntegerProperty(Object bean, String name, int initialValue) {
super(bean, name, initialValue);
ChangeListener<Number> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue.intValue());
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleObjectProperty instead
*/
@Deprecated
public class ImmediateObjectProperty<T> extends SimpleObjectProperty<T> {
@Override
@@ -48,31 +46,9 @@ public class ImmediateObjectProperty<T> extends SimpleObjectProperty<T> {
super.unbind();
}
private Consumer<T> consumer = null;
private ChangeListener<T> listener = null;
public void setChangedListener(Consumer<T> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<T> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<T> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateObjectProperty(Object bean, String name, T initialValue) {
super(bean, name, initialValue);
ChangeListener<T> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue);
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -25,9 +25,10 @@ import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleStringProperty instead
*/
@Deprecated
public class ImmediateStringProperty extends SimpleStringProperty {
@Override
@@ -63,6 +63,10 @@ public final class JavaVersion {
return platform;
}
public VersionNumber getVersionNumber() {
return VersionNumber.asVersion(longVersion.replace('_', '.'));
}
/**
* The major version of Java installation.
*
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@@ -40,7 +37,7 @@ public final class NetworkUtils {
private NetworkUtils() {
}
private static Supplier<String> userAgentSupplier = RandomUserAgent::randomUserAgent;
private static Supplier<String> userAgentSupplier = () -> "HMCLCore";
public static String getUserAgent() {
return userAgentSupplier.get();
@@ -156,4 +153,12 @@ public final class NetworkUtils {
return false;
}
}
public static boolean URLExists(URL url) throws IOException {
try (InputStream stream = url.openStream()) {
return true;
} catch (FileNotFoundException e) {
return false;
}
}
}
File diff suppressed because it is too large. Load diff