Compare commits

..
24 Commits
Author SHA1 Message Date
huangyuhui 7d2ea0b77e Add Tranditional Chinese translation 2018-07-01 13:37:09 +08:00
huangyuhui 1f30c299a5 Fix #360 2018-07-01 13:34:52 +08:00
huanghongxun c887d7e649 Merge pull request #358 from yushijinhun/authlib-injector
重构 authlib-injector 下载,并切换到新 API
2018-07-01 11:54:55 +08:00
yushijinhun 3fc7743a14 Add missing license header 2018-06-30 22:45:57 +08:00
yushijinhun 2a232f70db Refactor authlib-injector downloading, switch to new API 2018-06-30 22:45:57 +08:00
yushijinhun 6fae131287 Display (None) instead of empty string if no Yggdrasil server is available when adding account 2018-06-30 22:44:26 +08:00
yushijinhun 7ee7ef25f9 Refactor integrity check in FileDownloadTask, support multi algorithms 2018-06-30 22:43:57 +08:00
huangyuhui 2bd4bc1472 Add debug message in Task.run 2018-06-30 22:24:46 +08:00
huanghongxun aa170b647e Merge pull request #359 from yushijinhun/fixes
Fix bugs with yggdrasil authentication.
2018-06-30 22:12:06 +08:00
huangyuhui 4fa01216b1 Add debug message for tracing EmptyStackException and IllegalStateException 2018-06-30 22:09:35 +08:00
yushijinhun a82eb996a4 Add missing license header 2018-06-30 21:54:31 +08:00
yushijinhun 9315684ee3 Use CredentialExpiredException to indicate password login
instead of Non-ServerDisconnectException
2018-06-30 21:53:11 +08:00
huangyuhui 34f7774ce9 Fix NPE when the game launched and suddenly crashes 2018-06-30 21:36:57 +08:00
huangyuhui 75031a04cc Fix not on FX application thread 2018-06-30 21:28:41 +08:00
yushijinhun cfb0526cbb Fix potential NPE in YggdrasilAccount(selectedProfile is null) 2018-06-30 20:54:27 +08:00
yushijinhun 9fa56a9cd4 Regard token to be invalid only when remote throws ForbiddenOperationException 2018-06-30 18:52:37 +08:00
huangyuhui c4db9c9e12 Always show the advanced version setting pane as some people cannot find the options 2018-06-27 01:06:43 +08:00
huangyuhui 71272371e4 Fix #356 2018-06-26 21:55:29 +08:00
huangyuhui 91c6c4d233 Reduce size of welcome.png 2018-06-23 22:37:41 +08:00
huangyuhui 8a6653a926 Only show welcome page to Chinese 2018-06-23 22:25:39 +08:00
huangyuhui 76caf84e39 Welcome page 2018-06-23 22:15:23 +08:00
huangyuhui 0fee286b84 Use BMCLAPI as default ownload provider 2018-06-23 19:42:36 +08:00
huangyuhui 4af18a1575 Change build status icon in README.md 2018-06-23 13:03:28 +08:00
huangyuhui d9293f4da8 Display remote error message when RemoteAuthenticationException thrown 2018-06-21 19:31:16 +08:00
47 changed files with 982 additions and 265 deletions

No files matched your search

@@ -160,7 +160,7 @@ public final class Launcher extends Application {
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String UPDATE_SERVER = "https://hmcl.huangyuhui.net";
public static final String UPDATE_SERVER = "https://www.huangyuhui.net";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -70,7 +71,7 @@ public class HMCLGameDownloadTask extends Task {
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
cache,
profile.getDependency().getProxy(),
version.getDownloadInfo().getSha1()
new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
}
@@ -22,10 +22,12 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.LauncherVisibility;
@@ -106,14 +108,12 @@ public final class LauncherHelper {
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(Launcher.i18n("account.methods"), variables -> {
try {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
} catch (AuthenticationException e) {
variables.set("account", account.logIn());
} catch (CredentialExpiredException e) {
variables.set("account", DialogController.logIn(account));
} catch (AuthenticationException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
}))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
@@ -172,13 +172,24 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
if (!success && !Controllers.isStopped()) {
Platform.runLater(() -> {
Controllers.closeDialog(launchingStepsPane);
if (executor.getLastException() != null)
Controllers.dialog(I18nException.getStackTrace(executor.getLastException()),
scriptFile == null ? Launcher.i18n("launch.failed") : Launcher.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
Controllers.closeDialog(launchingStepsPane);
Exception ex = executor.getLastException();
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException)
message = Launcher.i18n("modpack.type.curse.error");
else
message = I18nException.getStackTrace(ex);
Controllers.dialog(message,
scriptFile == null ? Launcher.i18n("launch.failed") : Launcher.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
}
}
});
}
launchingStepsPane.setExecutor(null);
@@ -22,19 +22,14 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Level;
@@ -55,7 +50,9 @@ public final class Accounts {
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector, Accounts::getOrCreateAuthlibInjectorServer))
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
public static String getAccountType(Account account) {
@@ -73,22 +70,6 @@ public final class Accounts {
return username + ":" + character;
}
private static String downloadAuthlibInjector() throws Exception {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.txt");
int buildNumber = 0;
try {
buildNumber = Integer.parseInt(FileUtils.readText(local));
} catch (IOException | NumberFormatException ignore) {
}
if (buildNumber < buildInfo.getBuildNumber()) {
new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run();
FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber()));
}
return jar.getAbsolutePath();
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return Settings.SETTINGS.authlibInjectorServers.stream()
.filter(server -> url.equals(server.getUrl()))
@@ -79,7 +79,7 @@ public final class Config implements Cloneable {
public final StringProperty localization = new SimpleStringProperty();
@SerializedName("downloadtype")
public final IntegerProperty downloadType = new SimpleIntegerProperty();
public final IntegerProperty downloadType = new SimpleIntegerProperty(1);
@SerializedName("configurations")
public final ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@@ -99,6 +99,9 @@ public final class Config implements Cloneable {
@SerializedName("logLines")
public final IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("firstLaunch")
public final BooleanProperty firstLaunch = new SimpleBooleanProperty(true);
public final ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@Override
@@ -53,7 +53,7 @@ public final class Locales {
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN);
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
@@ -116,6 +116,7 @@ public final class Profile {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
getGlobal().setGlobal(true); // always keep global.isGlobal = true
getGlobal().setUsesGlobal(true);
return getGlobal();
} else
return vs;
@@ -79,7 +79,11 @@ public class Settings {
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final boolean firstLaunch;
private Settings() {
firstLaunch = SETTINGS.firstLaunch.get();
loadProxy();
for (Iterator<Map<Object, Object>> iterator = SETTINGS.accounts.iterator(); iterator.hasNext();) {
@@ -140,6 +144,7 @@ public class Settings {
public void save() {
try {
SETTINGS.accounts.clear();
SETTINGS.firstLaunch.set(false);
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
@@ -152,6 +157,10 @@ public class Settings {
}
}
public boolean isFirstLaunch() {
return firstLaunch;
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.commonDirectory.get()) {
@Override
public void invalidated() {
@@ -36,14 +36,13 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -83,6 +82,7 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
cboServers.setItems(Settings.SETTINGS.authlibInjectorServers);
cboServers.setPromptText(Launcher.i18n("general.prompt.empty"));
// workaround: otherwise the combox will be black
if (!cboServers.getItems().isEmpty())
@@ -253,16 +253,22 @@ public class AddAccountPane extends StackPane {
}
public static String accountException(Exception exception) {
if (exception instanceof InvalidCredentialsException) {
return Launcher.i18n("account.failed.invalid_credentials");
} else if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_charactor");
if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return Launcher.i18n("account.failed.connect_authentication_server");
} else if (exception instanceof InvalidTokenException) {
return Launcher.i18n("account.failed.invalid_token");
} else if (exception instanceof InvalidPasswordException) {
return Launcher.i18n("account.failed.invalid_password");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials"))
return Launcher.i18n("account.failed.invalid_credentials");
else if (remoteMessage.contains("Invalid token"))
return Launcher.i18n("account.failed.invalid_token");
else if (remoteMessage.contains("Invalid username or password"))
return Launcher.i18n("account.failed.invalid_password");
}
return exception.getMessage();
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
@@ -157,7 +157,8 @@ public final class Controllers {
}
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
@@ -174,6 +175,10 @@ public final class Controllers {
getLeftPaneController().showUpdate();
}
public static boolean isStopped() {
return decorator == null;
}
public static void shutdown() {
mainPage = null;
settingsPage = null;
@@ -22,6 +22,10 @@ import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
@@ -38,6 +42,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -45,8 +50,10 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Locales;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
@@ -61,6 +68,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Locale;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -129,6 +137,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private JFXButton btnClose;
@FXML
private HBox navLeft;
@FXML
private ImageView welcomeView;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
@@ -172,6 +182,19 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
dialog.setDialogContainer(drawerWrapper);
dialog.setContent(dialogPane);
welcomeView.setCursor(Cursor.HAND);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> drawerWrapper.getChildren().remove(welcomeView))
);
nowAnimation.play();
});
if (!Settings.INSTANCE.isFirstLaunch() || Settings.INSTANCE.getLocale().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
@@ -46,7 +46,6 @@ import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.*;
import java.io.File;
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
@@ -173,10 +174,12 @@ public final class VersionItem extends StackPane {
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
});
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtils.limitSize(iconView, 32, 32);
@@ -107,8 +107,6 @@ public final class VersionSettingsController {
}
public void loadVersionSetting(Profile profile, String versionId) {
rootPane.getChildren().remove(advancedSettingsPane);
this.profile = profile;
this.versionId = versionId;
@@ -173,6 +171,8 @@ public final class VersionSettingsController {
else
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
globalItem.setToggleSelectedListener(newValue -> {
// do not call versionSettings.setUsesGlobal(true/false)
// because versionSettings can be the global one.
if ((Boolean) newValue.getUserData())
profile.globalizeVersionSetting(versionId);
else
@@ -235,14 +235,6 @@ public final class VersionSettingsController {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
@FXML
private void onShowAdvanced() {
if (!rootPane.getChildren().contains(advancedSettingsPane))
rootPane.getChildren().add(advancedSettingsPane);
else
rootPane.getChildren().remove(advancedSettingsPane);
}
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
@@ -102,10 +102,10 @@ public final class DownloadWizardProvider implements WizardProvider {
case 1:
return new ModpackPage(controller);
default:
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings);
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings + ", pages: " + controller.getPages());
}
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings);
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@@ -20,10 +20,7 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.*;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
@@ -47,6 +44,10 @@ public class WizardController implements Navigation {
this.provider = provider;
}
public List<Node> getPages() {
return Collections.unmodifiableList(pages);
}
@Override
public void onStart() {
Objects.requireNonNull(provider);
@@ -81,6 +82,10 @@ public class WizardController implements Navigation {
@Override
public void onPrev(boolean cleanUp) {
if (!canPrev()) {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
}
Node page = pages.pop();
if (cleanUp && page instanceof WizardPage)
((WizardPage) page).cleanup(settings);
@@ -23,6 +23,7 @@ import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
@@ -182,7 +183,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -232,7 +233,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -8,6 +8,8 @@
<?import org.jackhuang.hmcl.ui.construct.AdvancedListBox?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import java.lang.String?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<fx:root xmlns="http://javafx.com/javafx"
type="StackPane"
xmlns:fx="http://javafx.com/fxml">
@@ -44,6 +46,9 @@
</StackPane>
</center>
</BorderPane>
<ImageView fx:id="welcomeView">
<Image url="/assets/img/welcome.png" />
</ImageView>
</StackPane>
</center>
<top>
@@ -104,8 +104,8 @@
</right>
</BorderPane>
</ComponentList>
<HBox alignment="CENTER" style="-fx-padding: 10 0 10 0;">
<JFXButton text="%settings.advanced" onMouseClicked="#onShowAdvanced" buttonType="RAISED" styleClass="jfx-button-raised"/>
<HBox alignment="CENTER_LEFT" style="-fx-padding: 20 0 12 0;">
<Label text="%settings.advanced" style="-fx-text-fill: #616161;" />
</HBox>
<ComponentList fx:id="advancedSettingsPane" depth="1">
<JFXTextField labelFloat="true" promptText="%settings.advanced.jvm_args" styleClass="fit-width"
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

@@ -38,7 +38,7 @@ account.failed.connect_authentication_server=Cannot connect to the authenticatio
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_charactor=No character in this account.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.injector.add=Add authentication server
@@ -99,6 +99,8 @@ folder.resourcepacks=Resourcepacks
folder.saves=Saves
folder.screenshots=Screenshots
general.prompt.empty=(None)
input.email=The username must be an e-mail.
input.number=Must be a number.
input.not_empty=Required field
@@ -173,7 +175,7 @@ message.unknown=Unknown
modpack=Mod pack
modpack.choose=Choose a modpack zip file which you want to import. If you want to update the modpack, please enter the version you want to update.
modpack.choose=Choose a modpack zip file which you want to install.
modpack.desc=Describe your modpack, including precautions, changlog, supporting Markdown(also supporting online pictures).
modpack.enter_name=Enter your desired name for this game.
modpack.export=Export Modpack
@@ -202,6 +204,7 @@ modpack.task.install.error=Failed to install the modpack. Maybe the files is inc
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install relative files to Curse modpack
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
@@ -240,7 +243,7 @@ selector.custom=Custom
settings=Settings
settings.advanced=Advanced
settings.advanced=Advanced Settings
settings.advanced.dont_check_game_completeness=Don't check game completeness
settings.advanced.game_dir.default=Default (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except assets,libraries)
@@ -0,0 +1,334 @@
# Hello Minecraft! Launcher.
# Copyright (C) 2013 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/}.
#
#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.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.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
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.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.password=密碼
account.username=用戶名
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.sh=Bash 腳本
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
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.optifine=OptiFine
install.installer.version=%s 版本
install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=當前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
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.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 路徑。
launcher=啓動器
launcher.background=背景地址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
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.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
main_page=主頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.error=錯誤
message.info=提示
message.success=已完成
message.unknown=未知
modpack=整合包
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.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.saves=遊戲存檔
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.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.scan=解析整合包
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.hmcl=HMCL
modpack.type.multimc=MultiMC
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.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整合包會保存您目前的下載源設置
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.title=遊戲目錄
profile.selected=已選中
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
settings=普通設置
settings.advanced=高級設置
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.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.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.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
settings.game=遊戲設置
settings.game.dimension=遊戲窗口分辨率
settings.game.exploration=瀏覽
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.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=代理賬戶
settings.launcher.proxy.no_proxy=直連
settings.launcher.proxy.has_proxy=啓用驗證
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
update=啓動器更新
update.failed=檢查更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
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.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.settings=遊戲設置
version.update=更新
wizard.prev=< 上一步
wizard.failed=失敗
wizard.finish=完成
wizard.next=下一步 >
@@ -38,7 +38,7 @@ account.failed.connect_authentication_server=无法连接认证服务器,可能
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.injector.add=添加认证服务器
@@ -99,6 +99,8 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹
folder.screenshots=截图文件夹
general.prompt.empty=(无)
input.email=用户名必须是邮箱
input.number=必须是数字
input.not_empty=必填项
@@ -173,7 +175,7 @@ message.unknown=未知
modpack=整合包
modpack.choose=选择要导入的游戏整合包文件,如果您希望更新整合包,请输入要更新的版本名
modpack.choose=选择要安装的游戏整合包文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
@@ -202,6 +204,7 @@ modpack.task.install.error=安装失败,可能是整合包格式不正确或
modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

@@ -47,10 +47,9 @@ public abstract class Account {
/**
* Login with stored credentials.
*
* @throws ServerDisconnectException if an network error has occurred, in which case password login won't be tried.
* @throws AuthenticationException if an error has occurred. If it's not a {@link ServerDisconnectException}, password login will be tried.
* @throws CredentialExpiredException when the stored credentials has expired, in which case a password login will be performed
*/
public abstract AuthInfo logIn() throws ServerDisconnectException, AuthenticationException;
public abstract AuthInfo logIn() throws CredentialExpiredException, AuthenticationException;
/**
* Login with specified password.
@@ -1,7 +1,7 @@
/*
* 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
@@ -18,8 +18,25 @@
package org.jackhuang.hmcl.auth;
/**
* Thrown when the stored credentials has expired.
* This exception indicates that a password login should be performed.
*
* @author huangyuhui
* @author yushijinhun
* @see Account#logIn()
*/
public class InvalidTokenException extends AuthenticationException {
public class CredentialExpiredException extends AuthenticationException {
public CredentialExpiredException() {}
public CredentialExpiredException(String message, Throwable cause) {
super(message, cause);
}
public CredentialExpiredException(String message) {
super(message);
}
public CredentialExpiredException(Throwable cause) {
super(cause);
}
}
@@ -1,25 +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.auth;
/**
* throws if wrong password or logging fails for too many times.
* @author huangyuhui
*/
public final class InvalidCredentialsException extends AuthenticationException {
}
@@ -1,3 +1,20 @@
/*
* 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.auth;
public class ServerResponseMalformedException extends AuthenticationException {
@@ -31,19 +31,23 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private final AuthlibInjectorServer server;
private final ExceptionalSupplier<String, ?> injectorJarPath;
private AuthlibInjectorServer server;
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<String, ?> injectorJarPath, String username, UUID characterUUID, YggdrasilSession session) {
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
this.injectorJarPath = injectorJarPath;
this.authlibInjectorDownloader = authlibInjectorDownloader;
this.server = server;
}
@@ -57,25 +61,42 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return inject(() -> super.logInWithPassword(password, selector));
}
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> supplier) throws AuthenticationException {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time.
GetTask getTask = new GetTask(NetworkUtils.toURL(server.getUrl()));
AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = Lang.thread(() -> flag.set(getTask.test()));
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
// Pre-fetch metadata
GetTask metadataFetchTask = new GetTask(NetworkUtils.toURL(server.getUrl()));
Thread metadataFetchThread = Lang.thread(() -> {
try {
metadataFetchTask.run();
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to pre-fetch Yggdrasil metadata", e);
}
}, "Yggdrasil metadata fetch thread");
AuthInfo info = supplier.get();
// Update authlib-injector
AuthlibInjectorArtifactInfo artifact;
try {
thread.join();
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + injectorJarPath.get() + "=" + server.getUrl());
if (flag.get())
arguments = arguments.addJVMArguments("-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), UTF_8));
return info.withArguments(arguments);
} catch (Exception e) {
throw new AuthenticationException("Unable to get authlib injector jar path", e);
artifact = authlibInjectorDownloader.get();
} catch (IOException e) {
throw new AuthenticationException("Failed to download authlib-injector", e);
}
// Perform authentication
AuthInfo info = loginAction.get();
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl());
// Wait for metadata to be fetched
try {
metadataFetchThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Optional<String> metadata = Optional.ofNullable(metadataFetchTask.getResult());
if (metadata.isPresent()) {
arguments = arguments.addJVMArguments(
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(metadata.get().getBytes(UTF_8)));
}
return info.withArguments(arguments);
}
@Override
@@ -1,3 +1,20 @@
/*
* 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.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory;
@@ -7,6 +24,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.ExceptionalSupplier;
import java.io.IOException;
import java.net.Proxy;
import java.util.Map;
import java.util.Objects;
@@ -15,14 +33,14 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
private Function<String, AuthlibInjectorServer> serverLookup;
/**
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
*/
public AuthlibInjectorAccountFactory(ExceptionalSupplier<String, ?> injectorJarPathSupplier, Function<String, AuthlibInjectorServer> serverLookup) {
this.injectorJarPathSupplier = injectorJarPathSupplier;
public AuthlibInjectorAccountFactory(ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, Function<String, AuthlibInjectorServer> serverLookup) {
this.authlibInjectorDownloader = authlibInjectorDownloader;
this.serverLookup = serverLookup;
}
@@ -36,7 +54,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, injectorJarPathSupplier, username, null, null);
server, authlibInjectorDownloader, username, null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -56,6 +74,6 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, injectorJarPathSupplier, username, session.getSelectedProfile().getId(), session);
server, authlibInjectorDownloader, username, session.getSelectedProfile().getId(), session);
}
}
@@ -0,0 +1,50 @@
/*
* 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.auth.authlibinjector;
import java.nio.file.Path;
public class AuthlibInjectorArtifactInfo {
private int buildNumber;
private String version;
private Path location;
public AuthlibInjectorArtifactInfo(int buildNumber, String version, Path location) {
this.buildNumber = buildNumber;
this.version = version;
this.location = location;
}
public int getBuildNumber() {
return buildNumber;
}
public String getVersion() {
return version;
}
public Path getLocation() {
return location;
}
@Override
public String toString() {
return "authlib-injector [buildNumber=" + buildNumber + ", version=" + version + "]";
}
}
@@ -1,59 +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.auth.authlibinjector;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.IOException;
@Immutable
public final class AuthlibInjectorBuildInfo {
private final int buildNumber;
private final String url;
public AuthlibInjectorBuildInfo() {
this(0, "");
}
public AuthlibInjectorBuildInfo(int buildNumber, String url) {
this.buildNumber = buildNumber;
this.url = url;
}
public int getBuildNumber() {
return buildNumber;
}
public String getUrl() {
return url;
}
public static AuthlibInjectorBuildInfo requestBuildInfo() throws IOException, JsonParseException {
return requestBuildInfo(UPDATE_URL);
}
public static AuthlibInjectorBuildInfo requestBuildInfo(String updateUrl) throws IOException, JsonParseException {
return JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(updateUrl)), AuthlibInjectorBuildInfo.class);
}
public static final String UPDATE_URL = "https://authlib-injector.to2mbn.org/api/buildInfo";
}
@@ -0,0 +1,158 @@
/*
* 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.auth.authlibinjector;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.logging.Level;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
public class AuthlibInjectorDownloader {
private static final String LATEST_BUILD_URL = "https://authlib-injector.yushi.moe/artifact/latest.json";
private Path artifactLocation;
private Supplier<DownloadProvider> downloadProvider;
/**
* @param artifactsDirectory where to save authlib-injector artifacts
*/
public AuthlibInjectorDownloader(Path artifactsDirectory, Supplier<DownloadProvider> downloadProvider) {
this.artifactLocation = artifactsDirectory.resolve("authlib-injector.jar");
this.downloadProvider = downloadProvider;
}
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
synchronized (artifactLocation) {
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
try {
update(local);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download authlib-injector", e);
if (!local.isPresent()) {
throw e;
}
LOG.warning("Fallback to use cached artifact: " + local.get());
}
return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized"));
}
}
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) {
return;
}
try {
new FileDownloadTask(new URL(downloadProvider.get().injectURL(latest.downloadUrl)), artifactLocation.toFile(), Proxy.NO_PROXY,
Optional.ofNullable(latest.checksums.get("sha256"))
.map(checksum -> new IntegrityCheck("SHA-256", checksum))
.orElse(null))
.run();
} catch (Exception e) {
throw new IOException("Failed to download authlib-injector", e);
}
LOG.info("Updated authlib-injector to " + latest.version);
}
private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException {
try {
return JsonUtils.fromNonNullJson(
NetworkUtils.doGet(
new URL(downloadProvider.get().injectURL(LATEST_BUILD_URL))),
AuthlibInjectorVersionInfo.class);
} catch (JsonParseException e) {
throw new IOException("Malformed response", e);
}
}
private Optional<AuthlibInjectorArtifactInfo> getLocalArtifact() {
if (!Files.isRegularFile(artifactLocation)) {
return Optional.empty();
}
try {
return Optional.of(readArtifactInfo(artifactLocation));
} catch (IOException e) {
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
return Optional.empty();
}
}
private static AuthlibInjectorArtifactInfo readArtifactInfo(Path location) throws IOException {
try (JarFile jarFile = new JarFile(location.toFile())) {
Attributes attributes = jarFile.getManifest().getMainAttributes();
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
if (!"authlib-injector".equals(title)) {
throw new IOException("Bad Implementation-Title");
}
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
int buildNumber;
try {
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
.map(Integer::parseInt)
.orElseThrow(() -> new IOException("Missing Build-Number"));
} catch (NumberFormatException e) {
throw new IOException("Bad Build-Number", e);
}
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
}
}
private class AuthlibInjectorVersionInfo {
@SerializedName("build_number")
public int buildNumber;
@SerializedName("version")
public String version;
@SerializedName("download_url")
public String downloadUrl;
@SerializedName("checksums")
public Map<String, String> checksums;
}
}
@@ -1,3 +1,20 @@
/*
* 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.auth.authlibinjector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
@@ -66,40 +66,54 @@ public class YggdrasilAccount extends Account {
@Override
public AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
logInWithToken();
selectProfile(new SpecificCharacterSelector(characterUUID));
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
} else {
try {
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
} catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
throw new CredentialExpiredException(e);
} else {
throw e;
}
}
}
}
return session.toAuthInfo();
}
@Override
public final AuthInfo logInWithPassword(String password) throws AuthenticationException {
public AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
}
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
session = service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID()));
selectProfile(selector);
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
return session.toAuthInfo();
}
private void selectProfile(CharacterSelector selector) throws AuthenticationException {
if (session.getSelectedProfile() == null) {
if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0)
/**
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
* has a profile selected.
*
* @param acquiredSession the session acquired by making an authenticate/refresh request
*/
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
if (acquiredSession.getSelectedProfile() == null) {
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
throw new NoCharacterException(this);
session = service.refresh(session.getAccessToken(), session.getClientToken(), selector.select(this, Arrays.asList(session.getAvailableProfiles())));
this.session = service.refresh(
acquiredSession.getAccessToken(),
acquiredSession.getClientToken(),
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
} else {
this.session = acquiredSession;
}
characterUUID = session.getSelectedProfile().getId();
}
private void logInWithToken() throws AuthenticationException {
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
return;
}
session = service.refresh(session.getAccessToken(), session.getClientToken(), null);
this.characterUUID = this.session.getSelectedProfile().getId();
}
@Override
@@ -83,8 +83,11 @@ public class YggdrasilService {
try {
requireEmpty(request(provider.getValidationURL(), createRequestWithCredentials(accessToken, clientToken)));
return true;
} catch (InvalidCredentialsException | InvalidTokenException e) {
return false;
} catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
return false;
}
throw e;
}
}
@@ -145,17 +148,6 @@ public class YggdrasilService {
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
if (!StringUtils.isBlank(response.error)) {
if (response.errorMessage != null && "ForbiddenOperationException".equals(response.error)) {
if (response.errorMessage.contains("Invalid credentials"))
throw new InvalidCredentialsException();
else if (response.errorMessage.contains("Invalid token"))
throw new InvalidTokenException();
else if (response.errorMessage.contains("Invalid username or password"))
throw new InvalidPasswordException();
}
throw new RemoteAuthenticationException(response.error, response.errorMessage, response.cause);
}
}
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.AssetObject;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -106,7 +107,7 @@ public final class GameAssetDownloadTask extends Task {
flag = !file.exists();
}
if (flag) {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, dependencyManager.getProxy(), assetObject.getHash());
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, dependencyManager.getProxy(), new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -56,7 +57,7 @@ public final class GameDownloadTask extends Task {
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
jar,
dependencyManager.getProxy(),
version.getDownloadInfo().getSha1()
new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
));
}
@@ -4,6 +4,7 @@ import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
@@ -48,7 +49,7 @@ public final class LibraryDownloadTask extends Task {
setSignificance(TaskSignificance.MODERATE);
task = new FileDownloadTask(NetworkUtils.toURL(url),
file, dependencyManager.getProxy(), library.getDownload().getSha1());
file, dependencyManager.getProxy(), new IntegrityCheck("SHA-1", library.getDownload().getSha1()));
}
@Override
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
* 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
@@ -15,10 +15,21 @@
* 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.auth;
package org.jackhuang.hmcl.mod;
/**
* throws if wrong password.
*/
public class InvalidPasswordException extends AuthenticationException {
public class CurseCompletionException extends Exception {
public CurseCompletionException() {
}
public CurseCompletionException(String message) {
super(message);
}
public CurseCompletionException(String message, Throwable cause) {
super(message, cause);
}
public CurseCompletionException(Throwable cause) {
super(cause);
}
}
@@ -24,9 +24,11 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -49,7 +51,7 @@ public final class CurseCompletionTask extends Task {
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
* @param version the existent and physical version.
*/
public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version) {
this(dependencyManager, version, null);
@@ -59,8 +61,8 @@ public final class CurseCompletionTask extends Task {
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
*/
public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version, CurseManifest manifest) {
this.dependencyManager = dependencyManager;
@@ -96,6 +98,7 @@ public final class CurseCompletionTask extends Task {
File root = repository.getVersionRoot(version);
File run = repository.getRunDirectory(version);
AtomicBoolean flag = new AtomicBoolean(true);
AtomicInteger finished = new AtomicInteger(0);
// Because in China, Curse is too difficult to visit,
@@ -104,14 +107,32 @@ public final class CurseCompletionTask extends Task {
manifest.getFiles().parallelStream()
.map(file -> {
updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
return Lang.ignoringException(() -> file.setFileName(NetworkUtils.detectFileName(file.getUrl(), dependencyManager.getProxy())), file);
if (StringUtils.isBlank(file.getFileName())) {
try {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl(), dependencyManager.getProxy()));
} catch (IOException ioe) {
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), ioe);
flag.set(false);
return file;
}
} else
return file;
})
.collect(Collectors.toList()));
FileUtils.writeText(new File(root, "manifest.json"), Constants.GSON.toJson(newManifest));
for (CurseManifestFile file : newManifest.getFiles())
if (StringUtils.isNotBlank(file.getFileName()))
dependencies.add(new FileDownloadTask(file.getUrl(), new File(run, "mods/" + file.getFileName()), dependencyManager.getProxy()));
if (StringUtils.isNotBlank(file.getFileName())) {
File dest = new File(run, "mods/" + file.getFileName());
if (!dest.exists())
dependencies.add(new FileDownloadTask(file.getUrl(), dest, dependencyManager.getProxy()));
}
// Let this task fail if the curse manifest has not been completed.
if (!flag.get())
dependencies.add(Task.of(() -> {
throw new CurseCompletionException();
}));
}
}
@@ -82,7 +82,7 @@ public final class CurseManifestFile implements Validation {
return NetworkUtils.toURL("https://minecraft.curseforge.com/projects/" + projectID + "/files/" + fileID + "/download");
}
public CurseManifestFile setFileName(String fileName) {
public CurseManifestFile withFileName(String fileName) {
return new CurseManifestFile(projectID, fileID, fileName, required);
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.ChecksumMismatchException;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -35,6 +36,7 @@ import java.net.URL;
import java.security.MessageDigest;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
/**
@@ -44,9 +46,38 @@ import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
*/
public class FileDownloadTask extends Task {
public static class IntegrityCheck {
private String algorithm;
private String checksum;
public IntegrityCheck(String algorithm, String checksum) {
this.algorithm = requireNonNull(algorithm);
this.checksum = requireNonNull(checksum);
}
public String getAlgorithm() {
return algorithm;
}
public String getChecksum() {
return checksum;
}
public MessageDigest createDigest() {
return getDigest(algorithm);
}
public void performCheck(MessageDigest digest) throws ChecksumMismatchException {
String actualChecksum = String.format("%1$040x", new BigInteger(1, digest.digest()));
if (!checksum.equalsIgnoreCase(actualChecksum)) {
throw new ChecksumMismatchException(algorithm, checksum, actualChecksum);
}
}
}
private final URL url;
private final File file;
private final String hash;
private final IntegrityCheck integrityCheck;
private final int retry;
private final Proxy proxy;
private final EventManager<FailedEvent<URL>> onFailed = new EventManager<>();
@@ -74,23 +105,23 @@ public class FileDownloadTask extends Task {
* @param url the URL of remote file.
* @param file the location that download to.
* @param proxy the proxy.
* @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/
public FileDownloadTask(URL url, File file, Proxy proxy, String hash) {
this(url, file, proxy, hash, 5);
public FileDownloadTask(URL url, File file, Proxy proxy, IntegrityCheck integrityCheck) {
this(url, file, proxy, integrityCheck, 5);
}
/**
* @param url the URL of remote file.
* @param file the location that download to.
* @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
* @param retry the times for retrying if downloading fails.
* @param proxy the proxy.
*/
public FileDownloadTask(URL url, File file, Proxy proxy, String hash, int retry) {
public FileDownloadTask(URL url, File file, Proxy proxy, IntegrityCheck integrityCheck, int retry) {
this.url = url;
this.file = file;
this.hash = hash;
this.integrityCheck = integrityCheck;
this.retry = retry;
this.proxy = proxy;
@@ -159,7 +190,7 @@ public class FileDownloadTask extends Task {
temp = FileUtils.createTempFile();
rFile = new RandomAccessFile(temp, "rw");
MessageDigest digest = getDigest("SHA-1");
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
@@ -175,8 +206,9 @@ public class FileDownloadTask extends Task {
if (read == -1)
break;
if (hash != null)
if (digest != null) {
digest.update(buffer, 0, read);
}
// Write buffer to file.
rFile.write(buffer, 0, read);
@@ -214,11 +246,9 @@ public class FileDownloadTask extends Task {
if (downloaded != contentLength)
throw new IllegalStateException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
// Check hash code
if (hash != null) {
String hashCode = String.format("%1$040x", new BigInteger(1, digest.digest()));
if (!hash.equalsIgnoreCase(hashCode))
throw new IllegalStateException("Unexpected hash code: " + hashCode + ", expected: " + hash);
// Integrity check
if (integrityCheck != null) {
integrityCheck.performCheck(digest);
}
return;
@@ -29,6 +29,7 @@ import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Level;
/**
* Disposable task.
@@ -185,6 +186,9 @@ public abstract class Task {
}
public final void run() throws Exception {
if (getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + getName());
for (Task task : getDependents())
doSubTask(task);
execute();
@@ -0,0 +1,46 @@
/*
* 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.util;
import java.io.IOException;
public class ChecksumMismatchException extends IOException {
private String algorithm;
private String expectedChecksum;
private String actualChecksum;
public ChecksumMismatchException(String algorithm, String expectedChecksum, String actualChecksum) {
super("Incorrect checksum (" + algorithm + "), expected: " + expectedChecksum + ", actual: " + actualChecksum);
this.algorithm = algorithm;
this.expectedChecksum = expectedChecksum;
this.actualChecksum = actualChecksum;
}
public String getAlgorithm() {
return algorithm;
}
public String getExpectedChecksum() {
return expectedChecksum;
}
public String getActualChecksum() {
return actualChecksum;
}
}
@@ -20,10 +20,8 @@ package org.jackhuang.hmcl.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
@@ -138,9 +136,9 @@ public final class NetworkUtils {
String disposition = conn.getHeaderField("Content-Disposition");
if (disposition == null || !disposition.contains("filename=")) {
String u = conn.getURL().toString();
return substringAfterLast(u, '/');
return Lang.invoke(() -> URLDecoder.decode(substringAfterLast(u, '/'), StandardCharsets.UTF_8.name()));
} else
return removeSurrounding(substringAfter(disposition, "filename="), "\"");
return Lang.invoke(() -> URLDecoder.decode(removeSurrounding(substringAfter(disposition, "filename="), "\""), StandardCharsets.UTF_8.name()));
}
public static URL toURL(String str) {
+1 -1
View File
@@ -1,4 +1,4 @@
# Hello Minecraft! Launcher [![Build Status](https://travis-ci.org/huanghongxun/HMCL.svg?branch=master)](https://travis-ci.org/huanghongxun/HMCL)
# Hello Minecraft! Launcher [![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
GPL v3, see http://www.gnu.org/licenses/gpl.html
## Introduction