Compare commits

..
1 Commits
Author SHA1 Message Date
huangyuhui 63ca5dd409 Fixed #296 2018-05-20 19:48:43 +08:00
93 changed files with 1842 additions and 1215 deletions

No files matched your search

+5 -5
View File
@@ -48,16 +48,16 @@ jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Created-By' : 'Copyright(c) 2013-2018 huangyuhui.',
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
'Main-Class' : mainClass,
'Multi-Release': "true"
}
doLast {
//new File("build/signed").mkdirs()
//ant.signjar(signedjar: archivePath, jar: archivePath,
// keystore: "HMCL.keystore", storepass: "123456",
// alias: "HMCL")
new File("build/signed").mkdirs()
ant.signjar(signedjar: archivePath, jar: archivePath,
keystore: "HMCL.keystore", storepass: "123456",
alias: "HMCL")
def messageDigest = MessageDigest.getInstance("SHA1")
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
@@ -71,11 +72,8 @@ public final class Launcher extends Application {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
try {
Logging.start(LOG_DIRECTORY);
Logging.start();
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
@@ -116,6 +114,21 @@ public final class Launcher extends Application {
});
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
}
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
@@ -148,9 +161,8 @@ public final class Launcher extends Application {
return String.format(i18n(key), formatArgs);
}
public static final File MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = getWorkingDirectory("hmcl");
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
@@ -160,7 +172,6 @@ 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 = "http://47.101.47.110";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -17,41 +17,24 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import javax.swing.*;
import java.io.File;
public final class Main {
public static void main(String[] args) {
checkJavaFX();
checkDirectoryPath();
Launcher.main(args);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
System.err.println("Exclamation mark(!) is not allowed in the path where HMCL is in. Forcibly exit.");
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
}
}
private static void checkJavaFX() {
try {
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit("JavaFX is missing.\n"
+ "If you are using Java 11 or above, please downgrade to Java 8 or 10.\n"
+ "If you are using OpenJDK, please ensure OpenJFX is included.");
}
}
private static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
String message = "Exclamation mark(!) is not allowed in the path where HMCL is in.\nThe path is " + currentDirectory;
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
} else
Launcher.main(args);
}
}
@@ -1,57 +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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
@@ -34,10 +34,13 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.net.Proxy;
import java.util.*;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
public final class AccountHelper {
public static final AccountHelper INSTANCE = new AccountHelper();
private AccountHelper() {}
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
@@ -70,8 +73,8 @@ public final class AccountHelper {
return new SkinLoadTask(account, proxy, true);
}
private static File getSkinFile(UUID uuid) {
return new File(SKIN_DIR, uuid + ".png");
private static File getSkinFile(String name) {
return new File(SKIN_DIR, name + ".png");
}
public static Image getSkin(YggdrasilAccount account) {
@@ -79,11 +82,9 @@ public final class AccountHelper {
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
UUID uuid = account.getUUID();
if (uuid == null)
return getSteveSkin(scaleRatio);
File file = getSkinFile(uuid);
if (account.getCharacter() == null)
return getDefaultSkin(account, scaleRatio);
File file = getSkinFile(account.getCharacter());
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(),
@@ -91,14 +92,15 @@ public final class AccountHelper {
original.getHeight() * scaleRatio,
false, false);
}
return getDefaultSkin(uuid, scaleRatio);
return getDefaultSkin(account, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
File file = getSkinFile(profile.getId());
String name = profile.getName();
File file = getSkinFile(name);
downloadSkin(account, profile, true, proxy);
if (!file.exists())
return getDefaultSkin(profile.getId(), scaleRatio);
return getDefaultSkin(account, scaleRatio);
String url = "file:" + file.getAbsolutePath();
return scale(url, scaleRatio);
@@ -150,7 +152,7 @@ public final class AccountHelper {
Optional<Texture> texture = account.getSkin(profile);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(profile.getId());
File file = getSkinFile(profile.getName());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
@@ -163,7 +165,7 @@ public final class AccountHelper {
Optional<Texture> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getUUID());
File file = getSkinFile(account.getCharacter());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
@@ -185,8 +187,11 @@ public final class AccountHelper {
return scale("/assets/img/alex.png", 4);
}
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
int type = uuid.hashCode() & 1;
public static Image getDefaultSkin(Account account, double scaleRatio) {
if (account == null)
return getSteveSkin(scaleRatio);
int type = account.getUUID().hashCode() & 1;
if (type == 1)
return getAlexSkin(scaleRatio);
else
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
@@ -17,7 +17,9 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
@@ -36,6 +38,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
@@ -44,9 +47,7 @@ import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import java.util.function.Consumer;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
@@ -97,8 +98,10 @@ public final class LauncherHelper {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
if (account.canPlayOffline())
variables.set("account", account.playOffline());
else
throw e;
}
} catch (AuthenticationException e) {
variables.set("account", DialogController.logIn(account));
@@ -107,13 +110,7 @@ public final class LauncherHelper {
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
@@ -184,7 +181,6 @@ public final class LauncherHelper {
if (java == null) {
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
}
@@ -304,10 +300,10 @@ public final class LauncherHelper {
if (authInfo == null)
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>"),
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
pair(authInfo.getUsername(), "<player>")
forbiddenTokens = Lang.mapOf(
new Pair<>(authInfo.getAccessToken(), "<access token>"),
new Pair<>(authInfo.getUserId(), "<uuid>"),
new Pair<>(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
@@ -337,7 +333,7 @@ public final class LauncherHelper {
else
System.out.print(log);
logs.add(pair(log, level));
logs.add(new Pair<>(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
logs.removeFirst();
@@ -32,9 +32,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
@@ -42,9 +40,6 @@ import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
*/
@@ -55,10 +50,10 @@ public final class Accounts {
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
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))
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
new Pair<>(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
);
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
@@ -17,92 +17,256 @@
*/
package org.jackhuang.hmcl.setting;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import org.jackhuang.hmcl.Launcher;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.util.JavaVersion;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import java.util.*;
public final class Config implements Cloneable {
@SerializedName("last")
public final StringProperty selectedProfile = new SimpleStringProperty("");
private String selectedProfile = "";
@SerializedName("backgroundType")
public final IntegerProperty backgroundImageType = new SimpleIntegerProperty(0);
private int backgroundImageType = 0;
@SerializedName("bgpath")
public final StringProperty backgroundImage = new SimpleStringProperty();
private String backgroundImage = null;
@SerializedName("commonpath")
public final StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
private String commonDirectory = Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
@SerializedName("hasProxy")
public final BooleanProperty hasProxy = new SimpleBooleanProperty();
private boolean hasProxy = false;
@SerializedName("hasProxyAuth")
public final BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
private boolean hasProxyAuth = false;
@SerializedName("proxyType")
public final IntegerProperty proxyType = new SimpleIntegerProperty();
private int proxyType = 0;
@SerializedName("proxyHost")
public final StringProperty proxyHost = new SimpleStringProperty();
private String proxyHost = null;
@SerializedName("proxyPort")
public final StringProperty proxyPort = new SimpleStringProperty();
private String proxyPort = null;
@SerializedName("proxyUserName")
public final StringProperty proxyUser = new SimpleStringProperty();
private String proxyUser = null;
@SerializedName("proxyPassword")
public final StringProperty proxyPass = new SimpleStringProperty();
private String proxyPass = null;
@SerializedName("theme")
public final StringProperty theme = new SimpleStringProperty();
private String theme = null;
@SerializedName("java")
private List<JavaVersion> java = null;
@SerializedName("localization")
public final StringProperty localization = new SimpleStringProperty();
private String localization;
@SerializedName("downloadtype")
public final IntegerProperty downloadType = new SimpleIntegerProperty();
private int downloadType = 0;
@SerializedName("configurations")
public final ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
private Map<String, Profile> configurations = new TreeMap<>();
@SerializedName("accounts")
public final ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
private List<Map<Object, Object>> accounts = new LinkedList<>();
@SerializedName("selectedAccount")
public final StringProperty selectedAccount = new SimpleStringProperty("");
private String selectedAccount = "";
@SerializedName("fontFamily")
public final StringProperty fontFamily = new SimpleStringProperty("Consolas");
private String fontFamily = "Consolas";
@SerializedName("fontSize")
public final DoubleProperty fontSize = new SimpleDoubleProperty(12);
private double fontSize = 12;
@SerializedName("logLines")
public final IntegerProperty logLines = new SimpleIntegerProperty(100);
private int logLines = 100;
@SerializedName("authlibInjectorServerURLs")
public final ObservableSet<String> authlibInjectorServerURLs = FXCollections.observableSet(new HashSet<>());
private Set<String> authlibInjectorServerURLs = new HashSet<>();
public String getSelectedProfile() {
return selectedProfile;
}
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile = selectedProfile;
}
public String getBackgroundImage() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
}
public int getBackgroundImageType() {
return backgroundImageType;
}
public void setBackgroundImageType(int backgroundImageType) {
this.backgroundImageType = backgroundImageType;
}
public String getCommonDirectory() {
return commonDirectory;
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory = commonDirectory;
}
public boolean isHasProxy() {
return hasProxy;
}
public void setHasProxy(boolean hasProxy) {
this.hasProxy = hasProxy;
}
public boolean isHasProxyAuth() {
return hasProxyAuth;
}
public void setHasProxyAuth(boolean hasProxyAuth) {
this.hasProxyAuth = hasProxyAuth;
}
public int getProxyType() {
return proxyType;
}
public void setProxyType(int proxyType) {
this.proxyType = proxyType;
}
public String getProxyHost() {
return proxyHost;
}
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
}
public String getProxyPort() {
return proxyPort;
}
public void setProxyPort(String proxyPort) {
this.proxyPort = proxyPort;
}
public String getProxyUser() {
return proxyUser;
}
public void setProxyUser(String proxyUser) {
this.proxyUser = proxyUser;
}
public String getProxyPass() {
return proxyPass;
}
public void setProxyPass(String proxyPass) {
this.proxyPass = proxyPass;
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
}
public List<JavaVersion> getJava() {
return java;
}
public void setJava(List<JavaVersion> java) {
this.java = java;
}
public String getLocalization() {
return localization;
}
public void setLocalization(String localization) {
this.localization = localization;
}
public int getDownloadType() {
return downloadType;
}
public void setDownloadType(int downloadType) {
this.downloadType = downloadType;
}
public Map<String, Profile> getConfigurations() {
return configurations;
}
public void setConfigurations(Map<String, Profile> configurations) {
this.configurations = configurations;
}
public List<Map<Object, Object>> getAccounts() {
return accounts;
}
public void setAccounts(List<Map<Object, Object>> accounts) {
this.accounts = accounts;
}
public String getSelectedAccount() {
return selectedAccount;
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount = selectedAccount;
}
public String getFontFamily() {
return fontFamily;
}
public void setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
}
public double getFontSize() {
return fontSize;
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
}
public int getLogLines() {
return logLines;
}
public void setLogLines(int logLines) {
this.logLines = logLines;
}
public Set<String> getAuthlibInjectorServerURLs() {
return authlibInjectorServerURLs;
}
public void setAuthlibInjectorServerURLs(Set<String> authlibInjectorServerURLs) {
this.authlibInjectorServerURLs = authlibInjectorServerURLs;
}
@Override
public Config clone() {
return Settings.GSON.fromJson(Settings.GSON.toJson(this), Config.class);
}
@@ -22,21 +22,16 @@ import com.google.gson.GsonBuilder;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.scene.text.Font;
import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.event.AccountLoadingEvent;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
@@ -47,23 +42,15 @@ import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class Settings {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
.setPrettyPrinting().create();
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
@@ -73,16 +60,18 @@ public class Settings {
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Config SETTINGS = initSettings();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final Map<String, Account> accounts = new HashMap<>();
private Settings() {
{
loadProxy();
for (Iterator<Map<Object, Object>> iterator = SETTINGS.accounts.iterator(); iterator.hasNext();) {
for (Iterator<Map<Object, Object>> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class).orElse(""));
if (factory == null) {
// unrecognized account type, so remove it.
iterator.remove();
@@ -101,6 +90,7 @@ public class Settings {
accounts.put(Accounts.getAccountId(account), account);
}
checkAuthlibInjectorServerURLs();
checkAuthlibInjectorAccounts();
checkProfileMap();
@@ -136,11 +126,11 @@ public class Settings {
public void save() {
try {
SETTINGS.accounts.clear();
SETTINGS.getAccounts().clear();
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
SETTINGS.accounts.add(storage);
SETTINGS.getAccounts().add(storage);
}
FileUtils.writeText(SETTINGS_FILE, GSON.toJson(SETTINGS));
@@ -149,12 +139,12 @@ public class Settings {
}
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.commonDirectory.get()) {
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.getCommonDirectory()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.commonDirectory.set(get());
SETTINGS.setCommonDirectory(get());
save();
}
};
@@ -171,7 +161,7 @@ public class Settings {
this.commonPath.set(commonPath);
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.localization.get());
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.getLocalization());
public Locales.SupportedLocale getLocale() {
return locale;
@@ -179,7 +169,7 @@ public class Settings {
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
SETTINGS.localization.set(Locales.getNameByLocale(locale));
SETTINGS.setLocalization(Locales.getNameByLocale(locale));
save();
}
@@ -189,7 +179,7 @@ public class Settings {
return proxy;
}
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.proxyType.get());
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.getProxyType());
public Proxy.Type getProxyType() {
return proxyType;
@@ -197,62 +187,58 @@ public class Settings {
public void setProxyType(Proxy.Type proxyType) {
this.proxyType = proxyType;
SETTINGS.proxyType.set(Proxies.PROXIES.indexOf(proxyType));
SETTINGS.setProxyType(Proxies.PROXIES.indexOf(proxyType));
save();
loadProxy();
}
public String getProxyHost() {
return SETTINGS.proxyHost.get();
return SETTINGS.getProxyHost();
}
public void setProxyHost(String proxyHost) {
SETTINGS.proxyHost.set(proxyHost);
SETTINGS.setProxyHost(proxyHost);
save();
}
public String getProxyPort() {
return SETTINGS.proxyPort.get();
return SETTINGS.getProxyPort();
}
public void setProxyPort(String proxyPort) {
SETTINGS.proxyPort.set(proxyPort);
SETTINGS.setProxyPort(proxyPort);
save();
}
public String getProxyUser() {
return SETTINGS.proxyUser.get();
return SETTINGS.getProxyUser();
}
public void setProxyUser(String proxyUser) {
SETTINGS.proxyUser.set(proxyUser);
SETTINGS.setProxyUser(proxyUser);
save();
}
public String getProxyPass() {
return SETTINGS.proxyPass.get();
return SETTINGS.getProxyPass();
}
public void setProxyPass(String proxyPass) {
SETTINGS.proxyPass.set(proxyPass);
SETTINGS.setProxyPass(proxyPass);
save();
}
public boolean hasProxy() {
return SETTINGS.hasProxy.get();
}
public boolean hasProxy() { return SETTINGS.isHasProxy(); }
public void setHasProxy(boolean hasProxy) {
SETTINGS.hasProxy.set(hasProxy);
SETTINGS.setHasProxy(hasProxy);
save();
}
public boolean hasProxyAuth() {
return SETTINGS.hasProxyAuth.get();
}
public boolean hasProxyAuth() { return SETTINGS.isHasProxyAuth(); }
public void setHasProxyAuth(boolean hasProxyAuth) {
SETTINGS.hasProxyAuth.set(hasProxyAuth);
SETTINGS.setHasProxyAuth(hasProxyAuth);
save();
}
@@ -283,21 +269,21 @@ public class Settings {
}
public Font getFont() {
return Font.font(SETTINGS.fontFamily.get(), SETTINGS.fontSize.get());
return Font.font(SETTINGS.getFontFamily(), SETTINGS.getFontSize());
}
public void setFont(Font font) {
SETTINGS.fontFamily.set(font.getFamily());
SETTINGS.fontSize.set(font.getSize());
SETTINGS.setFontFamily(font.getFamily());
SETTINGS.setFontSize(font.getSize());
save();
}
public int getLogLines() {
return Math.max(SETTINGS.logLines.get(), 100);
return Math.max(SETTINGS.getLogLines(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.logLines.set(logLines);
SETTINGS.setLogLines(logLines);
save();
}
@@ -306,26 +292,37 @@ public class Settings {
****************************************/
public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.authlibInjectorServerURLs;
return SETTINGS.getAuthlibInjectorServerURLs();
}
public void removeAuthlibInjectorServerURL(String serverURL) {
SETTINGS.authlibInjectorServerURLs.remove(serverURL);
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
SETTINGS.authlibInjectorServerURLs.add(serverURL);
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().add(serverURL);
save();
}
private void checkAuthlibInjectorServerURLs() {
if (SETTINGS.getAuthlibInjectorServerURLs() == null) {
SETTINGS.setAuthlibInjectorServerURLs(new HashSet<>());
save();
}
}
private void checkAuthlibInjectorAccounts() {
for (Account account : getAccounts()) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount injectorAccount = (AuthlibInjectorAccount) account;
if (!SETTINGS.authlibInjectorServerURLs.contains(injectorAccount.getServerBaseURL()))
if (!SETTINGS.getAuthlibInjectorServerURLs().contains(injectorAccount.getServerBaseURL()))
deleteAccount(account);
}
}
@@ -336,14 +333,14 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.downloadType.get());
return DownloadProviders.getDownloadProvider(SETTINGS.getDownloadType());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
SETTINGS.downloadType.set(index);
SETTINGS.setDownloadType(index);
save();
}
@@ -351,7 +348,7 @@ public class Settings {
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.selectedAccount.get())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
@Override
public Account get() {
Account a = super.get();
@@ -373,7 +370,7 @@ public class Settings {
public void invalidated() {
super.invalidated();
SETTINGS.selectedAccount.set(getValue() == null ? "" : Accounts.getAccountId(getValue()));
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
save();
}
};
@@ -393,8 +390,6 @@ public class Settings {
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
@@ -423,12 +418,12 @@ public class Settings {
* BACKGROUND *
****************************************/
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.backgroundImage.get()) {
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.getBackgroundImage()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.backgroundImage.set(get());
SETTINGS.setBackgroundImage(get());
save();
}
};
@@ -445,12 +440,12 @@ public class Settings {
this.backgroundImage.set(backgroundImage);
}
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.backgroundImageType.get())) {
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.getBackgroundImageType())) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.backgroundImageType.set(get().ordinal());
SETTINGS.setBackgroundImageType(get().ordinal());
save();
}
};
@@ -471,12 +466,12 @@ public class Settings {
* THEME *
****************************************/
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.theme.get()).orElse(Theme.BLUE)) {
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.getTheme()).orElse(Theme.BLUE)) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.theme.set(get().getName().toLowerCase());
SETTINGS.setTheme(get().getName().toLowerCase());
save();
}
};
@@ -500,19 +495,19 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.selectedProfile.get())) {
if (!hasProfile(SETTINGS.getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
SETTINGS.selectedProfile.set(selectedProfile);
SETTINGS.setSelectedProfile(selectedProfile);
save();
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.selectedProfile.get());
return getProfile(SETTINGS.getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.selectedProfile.get())) {
SETTINGS.selectedProfile.set(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(selectedProfile.getName());
save();
Schedulers.computation().schedule(this::onProfileChanged);
}
@@ -530,7 +525,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.configurations;
return SETTINGS.getConfigurations();
}
public Collection<Profile> getProfiles() {
@@ -26,9 +26,7 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
*
@@ -89,20 +87,6 @@ public final class VersionSetting {
javaProperty.set(java);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
* It's used to determine which JRE to use when multiple JREs match the selected Java version.
*/
public String getDefaultJavaPath() {
return defaultJavaPathProperty.get();
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
@@ -460,18 +444,12 @@ public final class VersionSetting {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
JavaVersion c = JavaVersion.getJREs().get(getJava());
if (c == null) {
setJava("Default");
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else
return c;
} else throw new Error();
}
@@ -497,7 +475,6 @@ public final class VersionSetting {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
@@ -558,7 +535,6 @@ public final class VersionSetting {
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
return obj;
}
@@ -595,7 +571,6 @@ public final class VersionSetting {
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
@@ -28,6 +29,8 @@ import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.AddAccountPane;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
@@ -41,8 +41,6 @@ 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.Validator;
@@ -65,23 +63,18 @@ public class AddAccountPane extends StackPane {
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXProgressBar progressBar;
@FXML private Label lblAddInjectorServer;
@FXML private Hyperlink linkAddInjectorServer;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept;
@FXML private StackPane acceptPane;
@FXML private JFXSpinner spinnerAccept;
private final Consumer<Region> finalization;
private final TransitionHandler transitionHandler;
public AddAccountPane(Consumer<Region> finalization) {
this.finalization = finalization;
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
transitionHandler = new TransitionHandler(acceptPane);
acceptPane.getChildren().setAll(btnAccept);
loadServers();
cboType.getItems().setAll(Launcher.i18n("account.methods.offline"), Launcher.i18n("account.methods.yggdrasil"), Launcher.i18n("account.methods.authlib_injector"));
@@ -89,8 +82,8 @@ public class AddAccountPane extends StackPane {
txtPassword.setVisible(newValue.intValue() != 0);
lblPassword.setVisible(newValue.intValue() != 0);
cboServers.setVisible(newValue.intValue() == 2);
linkManageInjectorServers.setVisible(newValue.intValue() == 2);
lblInjectorServer.setVisible(newValue.intValue() == 2);
linkAddInjectorServer.setVisible(newValue.intValue() == 2);
lblAddInjectorServer.setVisible(newValue.intValue() == 2);
validateAcceptButton();
});
cboType.getSelectionModel().select(0);
@@ -129,21 +122,13 @@ public class AddAccountPane extends StackPane {
}));
}
private void showSpinner() {
transitionHandler.setContent(spinnerAccept, ContainerAnimations.FADE.getAnimationProducer());
}
private void hideSpinner() {
transitionHandler.setContent(btnAccept, ContainerAnimations.FADE.getAnimationProducer());
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null);
showSpinner();
progressBar.setVisible(true);
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
AccountFactory<?> factory;
@@ -157,7 +142,7 @@ public class AddAccountPane extends StackPane {
return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy());
}).finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
hideSpinner();
progressBar.setVisible(false);
finalization.accept(this);
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
@@ -165,7 +150,7 @@ public class AddAccountPane extends StackPane {
} else {
lblCreationWarning.setText(accountException(exception));
}
hideSpinner();
progressBar.setVisible(false);
}).start();
}
@@ -175,7 +160,7 @@ public class AddAccountPane extends StackPane {
}
@FXML
private void onManageInjecterServers() {
private void onAddInjecterServer() {
finalization.accept(this);
Controllers.navigate(Controllers.getServersPage());
}
@@ -223,7 +208,6 @@ public class AddAccountPane extends StackPane {
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
@@ -17,6 +17,7 @@ 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.URLValidator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -45,6 +46,9 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML private JFXProgressBar progressBar;
@FXML private JFXButton btnAddNext;
@FXML private URLValidator validatorURL;
@FXML private URLValidator validatorHttp;
private final TransitionHandler transitionHandler;
{
@@ -55,6 +59,9 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
getChildren().remove(dialog);
dialog.setDialogContainer(this);
validatorURL.setMessage(Launcher.i18n("input.url"));
validatorHttp.setMessage(Launcher.i18n("input.url.http"));
txtServerIp.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerIp.validate()));
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXDialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -31,6 +33,7 @@ import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.Lang;
import java.util.function.Consumer;
@@ -115,8 +118,7 @@ public final class Controllers {
}
public static void dialog(Region content) {
if (decorator != null)
decorator.showDialog(content);
decorator.showDialog(content);
}
public static void dialog(String text) {
@@ -169,8 +171,6 @@ public final class Controllers {
}
public static void showUpdate() {
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -46,16 +47,16 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
public final class FXUtils {
private FXUtils() {
@@ -124,7 +125,7 @@ public final class FXUtils {
}
public static void removeListener(Node node, String key) {
tryCast(node.getProperties().get(key), ListenerPair.class)
Lang.cast(node.getProperties().get(key), ListenerPair.class)
.ifPresent(info -> {
info.unbind();
node.getProperties().remove(key);
@@ -200,6 +201,12 @@ public final class FXUtils {
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
}
public static void installTooltip(Node node, String tooltip) {
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
}
@@ -207,22 +214,17 @@ public final class FXUtils {
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
// Java 8
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
"install", node, tooltip);
} catch (Throwable e) {
try {
// Java 9
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
call(tooltip, "setShowDelay", new Duration(openDelay));
call(tooltip, "setShowDuration", new Duration(visibleDelay));
call(tooltip, "setHideDelay", new Duration(closeDelay));
} catch (Throwable e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
}
Tooltip.install(node, tooltip);
}
@@ -331,7 +333,7 @@ public final class FXUtils {
*/
@SuppressWarnings("unchecked")
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
ChangeListener listener = Lang.get(comboBox.getProperties(), "FXUtils.bindEnum.listener", ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
@@ -33,7 +33,6 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -51,7 +50,6 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@@ -62,7 +60,6 @@ public final class LeftPaneController {
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(Launcher.i18n("account.missing"), Launcher.i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
@@ -89,7 +86,6 @@ public final class LeftPaneController {
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
@@ -148,24 +144,11 @@ public final class LeftPaneController {
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
@@ -184,7 +167,7 @@ public final class LeftPaneController {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
item.setImage(AccountHelper.getDefaultSkin(account, 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount)
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
@@ -23,7 +23,6 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
@@ -43,17 +42,19 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
@@ -74,8 +75,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
private JFXSpinner spinner;
@FXML
private JFXMasonryPane masonryPane;
@FXML
private ScrollPane scrollPane;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
@@ -88,8 +87,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
// This will occupy 0.5s. Too slow!
JFXUtilities.runInFX(this::loadingVersions);
JFXUtilities.runInFXAndWait(this::loadingVersions);
});
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> {
this.profile = event.getProfile();
@@ -105,31 +103,28 @@ public final class MainPage extends StackPane implements DecoratorPage {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
private Node buildNode(HMCLGameRepository repository, Version version, Callable<String> gameCallable) {
private Node buildNode(HMCLGameRepository repository, Version version, String game) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(repository.isModpack(id));
Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> {
String game = vars.get("game");
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.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(Launcher.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(Launcher.i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
item.setLibraries(libraries.toString());
});
item.setGameVersion(game);
item.setVersionName(id);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.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(Launcher.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(Launcher.i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
item.setLibraries(libraries.toString());
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
@@ -174,7 +169,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
} catch (MismatchedModpackTypeException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
} catch (IOException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.invalid"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
@@ -192,7 +187,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
Launcher.i18n("modpack.export"),
Launcher.i18n("folder.game")
));
versionList.setOnMouseClicked(e -> {
versionList.setOnMouseClicked(e ->{
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
@@ -209,8 +204,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
break;
default:
break;
}
});
}});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
@@ -227,17 +221,18 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadingVersions() {
getChildren().setAll(spinner);
masonryPane.getChildren().clear();
FXUtils.resetChildren(masonryPane, Collections.emptyList());
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
List<Node> children = new LinkedList<>();
for (Version version : repository.getVersions()) {
children.add(buildNode(repository, version, GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")));
}
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
masonryPane.getChildren().setAll(children);
getChildren().setAll(contentPane);
FXUtils.resetChildren(masonryPane, children);
}
});
}
@@ -37,11 +37,9 @@ import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class ModController {
@@ -70,22 +68,21 @@ public final class ModController {
});
rootPane.setOnDragDropped(event -> {
List<File> files = event.getDragboard().getFiles();
if (files != null) {
Collection<File> mods = files.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!mods.isEmpty()) {
mods.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
List<File> mods = event.getDragboard().getFiles();
Stream<File> stream = null;
if (mods != null)
stream = mods.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
if (stream != null && stream.findAny().isPresent()) {
stream.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
event.consume();
});
@@ -21,8 +21,6 @@ import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
@@ -30,9 +28,10 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
@@ -66,107 +65,8 @@ public final class VersionItem extends StackPane {
private EventHandler<? super MouseEvent> launchClickedHandler = null;
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
content = new VBox();
{
header = new StackPane();
VBox.setVgrow(header, Priority.ALWAYS);
header.setPickOnBounds(false);
header.setPadding(new Insets(8));
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87);");
{
VBox headerContent = new VBox();
headerContent.setPadding(new Insets(8, 8, 0, 8));
{
lblVersionName = new Label();
lblVersionName.setStyle("-fx-font-size: 15;");
lblVersionName.setTextAlignment(TextAlignment.JUSTIFY);
lblVersionName.setWrapText(true);
headerContent.getChildren().add(lblVersionName);
lblGameVersion = new Label();
lblGameVersion.setStyle("-fx-font-size: 11;");
lblGameVersion.setTextAlignment(TextAlignment.JUSTIFY);
lblGameVersion.setWrapText(true);
headerContent.getChildren().add(lblGameVersion);
lblLibraries = new Label();
lblLibraries.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
lblLibraries.setTextAlignment(TextAlignment.JUSTIFY);
lblLibraries.setWrapText(true);
headerContent.getChildren().add(lblLibraries);
}
header.getChildren().add(headerContent);
}
content.getChildren().add(header);
body = new StackPane();
body.setStyle("-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;");
body.setMinHeight(40);
body.setPickOnBounds(false);
{
BorderPane bodyContent = new BorderPane();
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnSettings = new JFXButton();
btnSettings.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnSettings, 30);
FXUtils.setLimitHeight(btnSettings, 30);
hbox.getChildren().add(btnSettings);
btnUpdate = new JFXButton();
btnUpdate.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnUpdate, 30);
FXUtils.setLimitHeight(btnUpdate, 30);
hbox.getChildren().add(btnUpdate);
}
bodyContent.setLeft(hbox);
}
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnScript = new JFXButton();
btnScript.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnScript, 30);
FXUtils.setLimitHeight(btnScript, 30);
hbox.getChildren().add(btnScript);
btnLaunch = new JFXButton();
btnLaunch.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnLaunch, 30);
FXUtils.setLimitHeight(btnLaunch, 30);
hbox.getChildren().add(btnLaunch);
}
bodyContent.setRight(hbox);
}
body.getChildren().setAll(bodyContent);
}
content.getChildren().add(body);
}
getChildren().add(content);
icon = new StackPane();
StackPane.setAlignment(icon, Pos.TOP_RIGHT);
icon.setPickOnBounds(false);
{
iconView = new ImageView();
StackPane.setAlignment(iconView, Pos.CENTER_RIGHT);
StackPane.setMargin(iconView, new Insets(0, 12, 0, 0));
iconView.setImage(new Image("/assets/img/icon.png"));
icon.getChildren().add(iconView);
}
getChildren().add(icon);
}
public VersionItem() {
initializeComponents();
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
@@ -23,6 +23,7 @@ import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
@@ -43,7 +44,7 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Collection;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -83,14 +84,11 @@ public final class VersionSettingsController {
FXUtils.smoothScrolling(scroll);
Task.of(variables -> variables.set("list", JavaVersion.getJREs()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
initializeSelectedJava();
});
Task.of(variables -> variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
).collect(Collectors.toList()))).subscribe(Schedulers.javafx(), variables ->
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
@@ -153,17 +151,29 @@ public final class VersionSettingsController {
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
} else {
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
}
});
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
@@ -197,33 +207,9 @@ public final class VersionSettingsController {
lastVersionSetting = versionSetting;
initializeSelectedJava();
loadIcon();
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !JavaVersion.getJREsImmediately().isPresent() /* JREs are still being loaded */) {
return;
}
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(lastVersionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
@@ -22,7 +22,6 @@ import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public final class TransitionHandler implements AnimationHandler {
@@ -37,12 +36,6 @@ public final class TransitionHandler implements AnimationHandler {
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
}
@Override
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
@@ -32,6 +32,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -51,6 +52,7 @@ public class MultiFileItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -110,6 +112,10 @@ public class MultiFileItem extends ComponentList {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
}
public Node createChildren(String title) {
@@ -224,4 +230,16 @@ public class MultiFileItem extends ComponentList {
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public String getTooltip() {
return tooltip.get();
}
public StringProperty tooltipProperty() {
return tooltip;
}
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
}
}
@@ -1,6 +1,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
@@ -35,7 +35,6 @@ import java.io.File;
import java.util.Map;
import static org.jackhuang.hmcl.Launcher.i18n;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class DownloadWizardProvider implements WizardProvider {
private Profile profile;
@@ -68,9 +67,9 @@ public final class DownloadWizardProvider implements WizardProvider {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null;
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_CURSEFORGE_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
File selected = Lang.get(settings, ModpackPage.MODPACK_FILE, File.class).orElse(null);
Modpack modpack = Lang.get(settings, ModpackPage.MODPACK_CURSEFORGE_MANIFEST, Modpack.class).orElse(null);
String name = Lang.get(settings, ModpackPage.MODPACK_NAME, String.class).orElse(null);
if (selected == null || modpack == null || name == null) return null;
return ModpackHelper.getInstallTask(profile, selected, name, modpack);
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.ZipEngine;
import java.io.File;
@@ -82,16 +83,17 @@ public final class ExportWizardProvider implements WizardProvider {
try (ZipEngine zip = new ZipEngine(modpackFile)) {
Config config = Settings.INSTANCE.getRawConfig();
config.hasProxy.set(false);
config.selectedProfile.set("");
config.commonDirectory.set(null);
config.fontFamily.set("Consolas");
config.fontSize.set(12);
config.localization.set(null);
config.accounts.clear();
config.selectedAccount.set("");
config.logLines.set(100);
config.configurations.clear();
config.setHasProxy(false);
config.setSelectedProfile("");
config.setCommonDirectory(null);
config.setFontFamily("Consolas");
config.setFontSize(12);
config.setJava(null);
config.setLocalization(null);
config.setAccounts(null);
config.setSelectedAccount("");
config.setLogLines(100);
config.setConfigurations(null);
zip.putTextFile(Settings.GSON.toJson(config), Settings.SETTINGS_FILE_NAME);
zip.putFile(tempModpack, "modpack.zip");
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
@@ -41,9 +43,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
*/
@@ -155,19 +154,19 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
}
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
private static final Map<String, String> TRANSLATION = mapOf(
pair("minecraft/servers.dat", Launcher.i18n("modpack.files.servers_dat")),
pair("minecraft/saves", Launcher.i18n("modpack.files.saves")),
pair("minecraft/mods", Launcher.i18n("modpack.files.mods")),
pair("minecraft/config", Launcher.i18n("modpack.files.config")),
pair("minecraft/liteconfig", Launcher.i18n("modpack.files.liteconfig")),
pair("minecraft/resourcepacks", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/resources", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/options.txt", Launcher.i18n("modpack.files.options_txt")),
pair("minecraft/optionsshaders.txt", Launcher.i18n("modpack.files.optionsshaders_txt")),
pair("minecraft/mods/VoxelMods", Launcher.i18n("modpack.files.mods.voxelmods")),
pair("minecraft/dumps", Launcher.i18n("modpack.files.dumps")),
pair("minecraft/blueprints", Launcher.i18n("modpack.files.blueprints")),
pair("minecraft/scripts", Launcher.i18n("modpack.files.scripts"))
private static final Map<String, String> TRANSLATION = Lang.mapOf(
new Pair<>("minecraft/servers.dat", Launcher.i18n("modpack.files.servers_dat")),
new Pair<>("minecraft/saves", Launcher.i18n("modpack.files.saves")),
new Pair<>("minecraft/mods", Launcher.i18n("modpack.files.mods")),
new Pair<>("minecraft/config", Launcher.i18n("modpack.files.config")),
new Pair<>("minecraft/liteconfig", Launcher.i18n("modpack.files.liteconfig")),
new Pair<>("minecraft/resourcepacks", Launcher.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/resources", Launcher.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/options.txt", Launcher.i18n("modpack.files.options_txt")),
new Pair<>("minecraft/optionsshaders.txt", Launcher.i18n("modpack.files.optionsshaders_txt")),
new Pair<>("minecraft/mods/VoxelMods", Launcher.i18n("modpack.files.mods.voxelmods")),
new Pair<>("minecraft/dumps", Launcher.i18n("modpack.files.dumps")),
new Pair<>("minecraft/blueprints", Launcher.i18n("modpack.files.blueprints")),
new Pair<>("minecraft/scripts", Launcher.i18n("modpack.files.scripts"))
);
}
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
import java.util.List;
@@ -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.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
@@ -33,8 +34,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
@@ -49,24 +52,29 @@ import java.util.zip.GZIPInputStream;
*/
public class AppDataUpgrader extends IUpgrader {
private void launchNewerVersion(List<String> args, File jar) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException {
private boolean launchNewerVersion(List<String> args, File jar) throws IOException, PrivilegedActionException {
try (JarFile jarFile = new JarFile(jar)) {
String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClass == null)
throw new ClassNotFoundException("Main-Class not found in manifest");
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
} finally {
Logging.start(Launcher.LOG_DIRECTORY);
Thread.currentThread().setContextClassLoader(pre);
if (mainClass != null) {
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
return null;
});
} finally {
Logging.start();
Thread.currentThread().setContextClassLoader(pre);
}
return true;
}
}
return false;
}
@Override
@@ -82,18 +90,15 @@ public class AppDataUpgrader extends IUpgrader {
String j = m.get("loc");
if (j != null) {
File jar = new File(j);
if (jar.exists()) {
launchNewerVersion(args, jar);
if (jar.exists() && launchNewerVersion(args, jar))
System.exit(0);
}
}
}
}
} catch (JsonParseException ex) {
f.delete();
} catch (IOException | NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException | ClassNotFoundException t) {
} catch (IOException | PrivilegedActionException t) {
Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t);
AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version.
}
}
@@ -105,57 +110,60 @@ public class AppDataUpgrader extends IUpgrader {
checker.requestDownloadLink().then(Task.of(variables -> {
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
if (MessageBox.confirm(Launcher.i18n("update.newest_version", version.toString()) + "\n"
+ Launcher.i18n("update.should_open_link"),
MessageBox.YES_NO_OPTION) == MessageBox.YES_OPTION)
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Launcher.i18n("update.no_browser"));
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Launcher.i18n("update.no_browser"));
}
}
})).start();
}
@@ -193,7 +201,7 @@ public class AppDataUpgrader extends IUpgrader {
if (!FileUtils.makeDirectory(f.getParentFile()))
throw new IOException("Failed to make directories: " + f.getParent());
for (int i = 0; f.exists() && !f.delete(); i++)
for (int i = 0; f.exists(); i++)
f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar");
if (!f.createNewFile())
throw new IOException("Failed to create new file: " + f);
@@ -211,7 +219,7 @@ public class AppDataUpgrader extends IUpgrader {
public static class AppDataUpgraderJarTask extends Task {
public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl");
public static final File BASE_FOLDER = Launcher.getWorkingDirectory("hmcl");
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.VersionNumber;
@@ -34,8 +35,6 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huangyuhui
@@ -80,7 +79,7 @@ public class NewFileUpgrader extends IUpgrader {
File file = new File(realPath);
realPath = file.getAbsolutePath();
try {
realPath = java.net.URLDecoder.decode(realPath, UTF_8.name());
realPath = java.net.URLDecoder.decode(realPath, Charsets.DEFAULT_CHARSET.name());
} catch (Exception e) {
e.printStackTrace();
}
@@ -22,8 +22,6 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.OutOfDateEvent;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.Constants;
@@ -32,8 +30,6 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
@@ -69,20 +65,13 @@ public final class UpdateChecker {
*/
public TaskResult<VersionNumber> process(final boolean showMessage) {
return new TaskResult<VersionNumber>() {
GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION));
@Override
public Collection<? extends Task> getDependents() {
return value == null ? Collections.singleton(http) : Collections.emptyList();
}
@Override
public void execute() throws Exception {
if (Launcher.VERSION.contains("@"))
return;
if (value == null) {
versionString = http.getResult();
versionString = NetworkUtils.doGet(NetworkUtils.toURL("https://huangyuhui.duapp.com/hmcl/update.php?version=" + Launcher.VERSION));
value = VersionNumber.asVersion(versionString);
}
@@ -126,7 +115,7 @@ public final class UpdateChecker {
public void execute() {
if (download_link == null)
try {
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class);
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL("https://huangyuhui.duapp.com/hmcl/update_link.php")), Map.class);
} catch (JsonSyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to get update link.", e);
}
@@ -95,7 +95,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
Logging.LOG.log(Level.SEVERE, text);
if (checkThrowable(e)) {
if (checkThrowable(e) && !text.contains("OpenJDK")) {
Platform.runLater(() -> new CrashWindow(text).show());
if (!Launcher.UPDATE_CHECKER.isOutOfDate())
reportToServer(text);
@@ -911,14 +911,6 @@
-fx-stroke-width: 5.0;
}
.small-spinner {
-jfx-radius: 10;
}
.small-spinner > .arc {
-fx-stroke-width: 3.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -25,13 +25,13 @@
<JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.columnSpan="2"/>
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
<Label fx:id="lblAddInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<JFXComboBox fx:id="cboServers" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Hyperlink fx:id="linkManageInjectorServers" text="%account.injector.manage"
onMouseClicked="#onManageInjecterServers" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<Hyperlink fx:id="linkAddInjectorServer" text="%account.injector.add"
onMouseClicked="#onAddInjecterServer" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
@@ -56,11 +56,9 @@
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<StackPane fx:id="acceptPane">
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXSpinner fx:id="spinnerAccept" styleClass="small-spinner" />
</StackPane>
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER"/>
</fx:root>
@@ -34,9 +34,9 @@
<body>
<JFXTextField fx:id="txtServerIp" promptText="%account.injector.server_ip">
<validators>
<URLValidator message="%input.url">
<URLValidator fx:id="validatorURL">
</URLValidator>
<URLValidator message="%input.url.http">
<URLValidator fx:id="validatorHttp">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
@@ -12,7 +12,7 @@
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
</JFXMasonryPane>
</ScrollPane>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane" pickOnBounds="false" FXUtils.limitWidth="160" FXUtils.limitHeight="156">
<VBox fx:id="content" pickOnBounds="false">
<StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;">
<VBox style="-fx-padding: 8 8 0 8;">
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblGameVersion" style="-fx-font-size: 11;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblLibraries" style="-fx-font-size: 10; -fx-text-fill: gray;" textAlignment="JUSTIFY" wrapText="true" />
</VBox>
</StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40" pickOnBounds="false">
<BorderPane>
<left>
<HBox spacing="8">
<JFXButton fx:id="btnSettings" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
<JFXButton fx:id="btnUpdate" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
</HBox>
</left>
<right>
<HBox spacing="8">
<JFXButton fx:id="btnScript" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
<JFXButton fx:id="btnLaunch" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minHeight="30" minWidth="30" prefWidth="30" prefHeight="30" />
</HBox>
</right>
</BorderPane>
</StackPane>
</VBox>
<StackPane fx:id="icon" StackPane.alignment="TOP_RIGHT" pickOnBounds="false">
<ImageView fx:id="iconView" StackPane.alignment="CENTER_RIGHT">
<StackPane.margin>
<Insets right="12" />
</StackPane.margin>
<Image url="/assets/img/icon.png" />
</ImageView>
</StackPane>
</fx:root>
@@ -18,7 +18,7 @@
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=Author
about.author.statement=huanghongxun ([email protected])
about.author.statement=huanghongxun (MCF: klkl6523)
about.thanks_to=Thanks to
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors.
about.dependency=Dependency
@@ -39,8 +39,7 @@ account.failed.invalid_credentials=Incorrect password. Or forbidden to log in te
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to log in.
account.failed.no_charactor=No character in this account.
account.injector.add=Add authentication server
account.injector.manage=Manage authentication servers
account.injector.add=Add new authentication server
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
account.injector.server=Auth Server
account.injector.server_ip=Server URL
@@ -107,6 +106,7 @@ crash.advice.no=No advice.
crash.advice.no_lwjgl=Maybe drivers caused problems.
crash.advice.otherwise=Maybe mods caused problems.
crash.error=Minecraft has crashed.
crash.headless=If your OS is Linux, please use Oracle JDK instead of OpenJDK, or add "-Djava.awt.headless=false" JVM argument, or check if your Xserver works normally.
crash.launcher=Launcher has crashed!
crash.minecraft=Minecraft has crashed!
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
@@ -198,6 +198,7 @@ launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
launcher.exit_failed=Failed to shutdown.
launcher.modpack=Documentations for modpacks.
launcher.open_jdk=We have found that you started this application using OpenJDK, which will cause so many troubles drawing the UI. We suggest you using Oracle JDK instead.
launcher.restart=Options will be in operations only if restart this app.
launcher.tab.about=About
launcher.tab.general=General
@@ -18,7 +18,7 @@
about.copyright=版权
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.author.statement=huanghongxun (百度贴吧:huanghongxun20)
about.thanks_to=鸣谢
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有参与本项目,issue,pull requests的贡献者
about.dependency=依赖
@@ -39,8 +39,7 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.add=添加登录认证服务器
account.injector.http=警告:此服务器使用不安全的http协议,您的密码在登录时会被明文传输。
account.injector.server=认证服务器
account.injector.server_ip=服务器地址
@@ -107,6 +106,7 @@ crash.advice.no=无建议。
crash.advice.no_lwjgl=可能是游戏依赖库不完整或解压依赖库时出错。可以通过下载整合包解决问题。
crash.advice.otherwise=可能是Mod或其他问题。
crash.error=您的Minecraft崩溃了。
crash.headless=如果您的操作系统是Linux,请注意不要使用OpenJDK,务必使用Oracle JDK,或尝试添加-Djava.awt.headless=false参数,或检查您的Xserver是否正常
crash.launcher=启动器崩溃了!
crash.minecraft=Minecraft崩溃了!请认真阅读建议。
crash.user_fault=您的系统或Java环境可能安装不当导致本软件崩溃,请检查您的Java环境或您的电脑!可以尝试重新安装Java。
@@ -198,6 +198,7 @@ launcher.crash=Hello Minecraft!遇到了无法处理的错误,请复制下列
launcher.crash_out_dated=Hello Minecraft! Launcher遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
launcher.exit_failed=强制退出失败,可能是Forge 1.7.10及更高版本导致的,无法解决。
launcher.modpack=整合包作者帮助
launcher.open_jdk=我们发现您正在使用OpenJDK,这会导致很多界面问题,我们建议您更换Oracle JDK。
launcher.restart=本界面选项需要重启本启动器生效
launcher.tab.about=关于
launcher.tab.general=通用
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
@@ -30,38 +29,39 @@ import java.util.UUID;
public abstract class Account {
/**
* @return the name of the account who owns the character
* @return the account name (mostly email)
*/
public abstract String getUsername();
/**
*
* @return the character name
*/
public abstract String getCharacter();
/**
* @return the character UUID
* @return the UUID
*/
public abstract UUID getUUID();
/**
* 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.
* log in.
* @return the specific player's info.
* @throws AuthenticationException if server error occurred.
*/
public abstract AuthInfo logIn() throws ServerDisconnectException, AuthenticationException;
public abstract AuthInfo logIn() throws AuthenticationException;
/**
* Login with specified password.
*/
public abstract AuthInfo logInWithPassword(String password) throws AuthenticationException;
public abstract boolean canPlayOffline();
/**
* Play offline.
* @return the specific offline player's info.
*/
public abstract Optional<AuthInfo> playOffline();
public abstract AuthInfo playOffline();
public abstract void logOut();
public abstract Map<Object, Object> toStorage();
@@ -20,8 +20,6 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.Immutable;
import java.util.UUID;
/**
*
* @author huangyuhui
@@ -30,19 +28,29 @@ import java.util.UUID;
public final class AuthInfo {
private final String username;
private final UUID uuid;
private final String userId;
private final String accessToken;
private final UserType userType;
private final String userProperties;
private final Arguments arguments;
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
this(username, uuid, accessToken, userProperties, null);
public AuthInfo(String username, String userId, String accessToken) {
this(username, userId, accessToken, UserType.LEGACY);
}
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties, Arguments arguments) {
public AuthInfo(String username, String userId, String accessToken, UserType userType) {
this(username, userId, accessToken, userType, "{}");
}
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties) {
this(username, userId, accessToken, userType, userProperties, null);
}
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties, Arguments arguments) {
this.username = username;
this.uuid = uuid;
this.userId = userId;
this.accessToken = accessToken;
this.userType = userType;
this.userProperties = userProperties;
this.arguments = arguments;
}
@@ -51,14 +59,18 @@ public final class AuthInfo {
return username;
}
public UUID getUUID() {
return uuid;
public String getUserId() {
return userId;
}
public String getAccessToken() {
return accessToken;
}
public UserType getUserType() {
return userType;
}
/**
* Properties of this user.
* Don't know the difference between user properties and user property map.
@@ -69,14 +81,11 @@ public final class AuthInfo {
return userProperties;
}
/**
* @return null if no argument is specified
*/
public Arguments getArguments() {
return arguments;
}
public AuthInfo withArguments(Arguments arguments) {
return new AuthInfo(username, uuid, accessToken, userProperties, arguments);
public AuthInfo setArguments(Arguments arguments) {
return new AuthInfo(username, userId, accessToken, userType, userProperties, arguments);
}
}
@@ -20,24 +20,23 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import java.util.List;
import java.util.UUID;
/**
* Select character by name.
*/
public class SpecificCharacterSelector implements CharacterSelector {
private UUID uuid;
private final String id;
/**
* Constructor.
* @param uuid character's uuid.
* @param id character's name.
*/
public SpecificCharacterSelector(UUID uuid) {
this.uuid = uuid;
public SpecificCharacterSelector(String id) {
this.id = id;
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
return names.stream().filter(profile -> profile.getName().equals(id)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
}
}
@@ -0,0 +1,49 @@
/*
* 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;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
*
* @author huangyuhui
*/
public enum UserType {
LEGACY,
MOJANG;
public static UserType fromName(String name) {
return BY_NAME.get(name.toLowerCase());
}
public static UserType fromLegacy(boolean isLegacy) {
return isLegacy ? LEGACY : MOJANG;
}
static {
HashMap<String, UserType> byName = new HashMap<>();
for (UserType type : values())
byName.put(type.name().toLowerCase(), type);
BY_NAME = Collections.unmodifiableMap(byName);
}
public static final Map<String, UserType> BY_NAME;
}
@@ -25,23 +25,21 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.ExceptionalSupplier;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private final String serverBaseURL;
private final ExceptionalSupplier<String, ?> injectorJarPath;
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, String clientToken, String character, YggdrasilSession session) {
super(service, username, clientToken, character, session);
this.injectorJarPath = injectorJarPath;
this.serverBaseURL = serverBaseURL;
@@ -67,12 +65,13 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
try {
thread.join();
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL);
String arg = "-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL;
Arguments arguments = Arguments.addJVMArguments(null, arg);
if (flag.get())
arguments = arguments.addJVMArguments("-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), UTF_8));
arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), Charsets.UTF_8));
return info.withArguments(arguments);
return info.setArguments(arguments);
} catch (Exception e) {
throw new AuthenticationException("Unable to get authlib injector jar path", e);
}
@@ -6,13 +6,14 @@ import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.ExceptionalSupplier;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
import java.util.Map;
import java.util.Objects;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import java.util.UUID;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
@@ -32,7 +33,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
throw new IllegalArgumentException("Additional data should be server base url string for authlib injector accounts.");
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider((String) serverBaseURL), proxy),
(String) serverBaseURL, injectorJarPathSupplier, username, null, null);
(String) serverBaseURL, injectorJarPathSupplier, username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -42,14 +43,16 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
Objects.requireNonNull(storage);
Objects.requireNonNull(proxy);
YggdrasilSession session = YggdrasilSession.fromStorage(storage);
String username = tryCast(storage.get("username"), String.class)
String username = Lang.get(storage, "username", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
String apiRoot = tryCast(storage.get("serverBaseURL"), String.class)
String clientToken = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
String character = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
String apiRoot = Lang.get(storage, "serverBaseURL", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have API root."));
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(apiRoot), proxy),
apiRoot, injectorJarPathSupplier, username, session.getSelectedProfile().getId(), session);
apiRoot, injectorJarPathSupplier, username, clientToken, character, YggdrasilSession.fromStorage(storage));
}
}
@@ -20,18 +20,12 @@ package org.jackhuang.hmcl.auth.offline;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.*;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
*
* @author huang
@@ -39,9 +33,9 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public class OfflineAccount extends Account {
private final String username;
private final UUID uuid;
private final String uuid;
OfflineAccount(String username, UUID uuid) {
OfflineAccount(String username, String uuid) {
Objects.requireNonNull(username);
Objects.requireNonNull(uuid);
@@ -54,7 +48,7 @@ public class OfflineAccount extends Account {
@Override
public UUID getUUID() {
return uuid;
return UUIDTypeAdapter.fromString(uuid);
}
@Override
@@ -72,7 +66,7 @@ public class OfflineAccount extends Account {
if (StringUtils.isBlank(username))
throw new AuthenticationException("Username cannot be empty");
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
return new AuthInfo(username, uuid, uuid);
}
@Override
@@ -81,15 +75,25 @@ public class OfflineAccount extends Account {
}
@Override
public Optional<AuthInfo> playOffline() {
return Optional.empty();
public void logOut() {
// Offline account need not log out.
}
@Override
public boolean canPlayOffline() {
return false;
}
@Override
public AuthInfo playOffline() {
throw new IllegalStateException();
}
@Override
public Map<Object, Object> toStorage() {
return mapOf(
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
pair("username", username)
return Lang.mapOf(
new Pair<>("uuid", uuid),
new Pair<>("username", username)
);
}
@@ -19,14 +19,15 @@ package org.jackhuang.hmcl.auth.offline;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
import java.util.Map;
import java.util.UUID;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import java.util.logging.Level;
/**
*
@@ -45,17 +46,19 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
@Override
public OfflineAccount fromStorage(Map<Object, Object> storage, Proxy proxy) {
String username = tryCast(storage.get("username"), String.class)
String username = Lang.get(storage, "username", String.class)
.orElseThrow(() -> new IllegalStateException("Offline account configuration malformed."));
UUID uuid = tryCast(storage.get("uuid"), String.class)
.map(UUIDTypeAdapter::fromString)
String uuid = Lang.get(storage, "uuid", String.class)
.orElse(getUUIDFromUserName(username));
// Check if the uuid is vaild
UUIDTypeAdapter.fromString(uuid);
return new OfflineAccount(username, uuid);
}
private static UUID getUUIDFromUserName(String username) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(UTF_8));
private static String getUUIDFromUserName(String username) {
return DigestUtils.md5Hex(username);
}
}
@@ -0,0 +1,66 @@
/*
* 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.yggdrasil;
/**
*
* @author huangyuhui
*/
final class AuthenticationResponse extends ErrorResponse {
private final String accessToken;
private final String clientToken;
private final GameProfile selectedProfile;
private final GameProfile[] availableProfiles;
private final User user;
public AuthenticationResponse() {
this(null, null, null, null, null, null, null, null);
}
public AuthenticationResponse(String accessToken, String clientToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user, String error, String errorMessage, String cause) {
super(error, errorMessage, cause);
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
this.user = user;
}
public String getAccessToken() {
return accessToken;
}
public String getClientToken() {
return clientToken;
}
public GameProfile getSelectedProfile() {
return selectedProfile;
}
public GameProfile[] getAvailableProfiles() {
return availableProfiles;
}
public User getUser() {
return user;
}
}
@@ -0,0 +1,29 @@
package org.jackhuang.hmcl.auth.yggdrasil;
class ErrorResponse {
private final String error;
private final String errorMessage;
private final String cause;
public ErrorResponse() {
this(null, null, null);
}
public ErrorResponse(String error, String errorMessage, String cause) {
this.error = error;
this.errorMessage = errorMessage;
this.cause = cause;
}
public String getError() {
return error;
}
public String getErrorMessage() {
return errorMessage;
}
public String getCause() {
return cause;
}
}
@@ -17,8 +17,11 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*;
import org.jackhuang.hmcl.auth.UserType;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type;
import java.util.UUID;
/**
@@ -31,6 +34,7 @@ public final class GameProfile {
private final UUID id;
private final String name;
private final PropertyMap properties;
private final boolean legacy;
public GameProfile() {
this(null, null);
@@ -41,9 +45,14 @@ public final class GameProfile {
}
public GameProfile(UUID id, String name, PropertyMap properties) {
this(id, name, properties, false);
}
public GameProfile(UUID id, String name, PropertyMap properties, boolean legacy) {
this.id = id;
this.name = name;
this.properties = properties;
this.legacy = legacy;
}
public UUID getId() {
@@ -54,11 +63,46 @@ public final class GameProfile {
return name;
}
/**
* @return nullable
*/
public PropertyMap getProperties() {
return properties;
}
public boolean isLegacy() {
return legacy;
}
public UserType getUserType() {
return UserType.fromLegacy(isLegacy());
}
public static class Serializer implements JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(GameProfile src, Type type, JsonSerializationContext context) {
JsonObject result = new JsonObject();
if (src.getId() != null)
result.add("id", context.serialize(src.getId()));
if (src.getName() != null)
result.addProperty("name", src.getName());
return result;
}
@Override
public GameProfile deserialize(JsonElement je, Type type, JsonDeserializationContext context) throws JsonParseException {
if (!(je instanceof JsonObject))
throw new JsonParseException("The json element is not a JsonObject.");
JsonObject json = (JsonObject) je;
UUID id = json.has("id") ? context.deserialize(json.get("id"), UUID.class) : null;
String name = json.has("name") ? json.getAsJsonPrimitive("name").getAsString() : null;
return new GameProfile(id, name);
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.yggdrasil;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
@Immutable
public final class TextureResponse {
private final UUID profileId;
private final String profileName;
private final Map<TextureType, Texture> textures;
public TextureResponse() {
this(UUID.randomUUID(), "", Collections.emptyMap());
}
public TextureResponse(UUID profileId, String profileName, Map<TextureType, Texture> textures) {
this.profileId = profileId;
this.profileName = profileName;
this.textures = textures;
}
public UUID getProfileId() {
return profileId;
}
public String getProfileName() {
return profileName;
}
public Map<TextureType, Texture> getTextures() {
return textures == null ? null : Collections.unmodifiableMap(textures);
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
@@ -33,13 +34,15 @@ public class YggdrasilAccount extends Account {
private final YggdrasilService service;
private boolean isOnline = false;
private YggdrasilSession session;
private UUID characterUUID;
private final String clientToken;
private String character;
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
protected YggdrasilAccount(YggdrasilService service, String username, String clientToken, String character, YggdrasilSession session) {
this.service = service;
this.username = username;
this.session = session;
this.characterUUID = characterUUID;
this.clientToken = clientToken;
this.character = character;
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
this.session = null;
@@ -67,20 +70,20 @@ public class YggdrasilAccount extends Account {
public AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
logInWithToken();
selectProfile(new SpecificCharacterSelector(characterUUID));
selectProfile(new SpecificCharacterSelector(character));
}
return session.toAuthInfo();
return toAuthInfo();
}
@Override
public final AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
return logInWithPassword(password, new SpecificCharacterSelector(character));
}
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
session = service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID()));
session = service.authenticate(username, password, clientToken);
selectProfile(selector);
return session.toAuthInfo();
return toAuthInfo();
}
private void selectProfile(CharacterSelector selector) throws AuthenticationException {
@@ -88,42 +91,61 @@ public class YggdrasilAccount extends Account {
if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0)
throw new NoCharacterException(this);
session = service.refresh(session.getAccessToken(), session.getClientToken(), selector.select(this, Arrays.asList(session.getAvailableProfiles())));
session.setSelectedProfile(selector.select(this, Arrays.asList(session.getAvailableProfiles())));
}
characterUUID = session.getSelectedProfile().getId();
character = session.getSelectedProfile().getName();
}
private void logInWithToken() throws AuthenticationException {
if (service.validate(session.getAccessToken(), session.getClientToken())) {
if (service.validate(session.getAccessToken(), clientToken)) {
isOnline = true;
return;
}
session = service.refresh(session.getAccessToken(), session.getClientToken(), null);
session = service.refresh(session.getAccessToken(), clientToken);
}
private AuthInfo toAuthInfo() {
GameProfile profile = session.getSelectedProfile();
return new AuthInfo(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), session.getAccessToken(), profile.getUserType(),
new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create().toJson(Optional.ofNullable(session.getUser()).map(User::getProperties).orElseGet(PropertyMap::new)));
}
@Override
public Optional<AuthInfo> playOffline() {
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
return Optional.of(session.toAuthInfo());
public boolean canPlayOffline() {
return isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline();
}
return Optional.empty();
@Override
public AuthInfo playOffline() {
if (!canPlayOffline())
throw new IllegalStateException("Current account " + this + " cannot play offline.");
return toAuthInfo();
}
@Override
public void logOut() {
isOnline = false;
session = null;
}
@Override
public Map<Object, Object> toStorage() {
if (session == null)
throw new IllegalStateException("No session is specified");
HashMap<Object, Object> storage = new HashMap<>();
storage.put("username", getUsername());
storage.putAll(session.toStorage());
storage.put("clientToken", clientToken);
storage.put("character", character);
if (session != null)
storage.putAll(session.toStorage());
return storage;
}
@Override
public UUID getUUID() {
if (session == null || session.getSelectedProfile() == null)
if (session == null)
return null;
else
return session.getSelectedProfile().getId();
@@ -135,7 +157,7 @@ public class YggdrasilAccount extends Account {
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
if (!service.getTextures(profile).isPresent()) {
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
session.setAvailableProfile(profile = service.getCompleteGameProfile(profile.getId()));
}
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
@@ -27,8 +28,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
/**
*
* @author huangyuhui
@@ -48,7 +47,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(password);
Objects.requireNonNull(proxy);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, null, null);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -58,12 +57,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(storage);
Objects.requireNonNull(proxy);
YggdrasilSession session = YggdrasilSession.fromStorage(storage);
String username = tryCast(storage.get("username"), String.class)
String username = Lang.get(storage, "username", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
String clientToken = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
String character = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, session.getSelectedProfile().getId(), session);
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, clientToken, character, YggdrasilSession.fromStorage(storage));
}
public static String randomToken() {
@@ -4,20 +4,13 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.*;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.util.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.liftFunction;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilService {
private final YggdrasilProvider provider;
@@ -38,39 +31,23 @@ public class YggdrasilService {
Objects.requireNonNull(clientToken);
Map<String, Object> request = new HashMap<>();
request.put("agent", mapOf(
pair("name", "Minecraft"),
pair("version", 1)
request.put("agent", Lang.mapOf(
new Pair<>("name", "Minecraft"),
new Pair<>("version", 1)
));
request.put("username", username);
request.put("password", password);
request.put("clientToken", clientToken);
request.put("requestUser", true);
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
return handle(request(provider.getAuthenticationURL(), request), clientToken);
}
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
Map<String, Object> request = new HashMap<>();
request.put("accessToken", accessToken);
request.put("clientToken", clientToken);
return request;
}
public YggdrasilSession refresh(String accessToken, String clientToken, GameProfile characterToSelect) throws AuthenticationException {
public YggdrasilSession refresh(String accessToken, String clientToken) throws AuthenticationException {
Objects.requireNonNull(accessToken);
Objects.requireNonNull(clientToken);
Map<String, Object> request = createRequestWithCredentials(accessToken, clientToken);
request.put("requestUser", true);
if (characterToSelect != null) {
request.put("selectedProfile", mapOf(
pair("id", characterToSelect.getId()),
pair("name", characterToSelect.getName())));
}
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
return handle(request(provider.getRefreshmentURL(), new RefreshRequest(accessToken, clientToken)), clientToken);
}
public boolean validate(String accessToken) throws AuthenticationException {
@@ -81,7 +58,7 @@ public class YggdrasilService {
Objects.requireNonNull(accessToken);
try {
requireEmpty(request(provider.getValidationURL(), createRequestWithCredentials(accessToken, clientToken)));
requireEmpty(request(provider.getValidationURL(), new RefreshRequest(accessToken, clientToken)));
return true;
} catch (InvalidCredentialsException | InvalidTokenException e) {
return false;
@@ -95,7 +72,7 @@ public class YggdrasilService {
public void invalidate(String accessToken, String clientToken) throws AuthenticationException {
Objects.requireNonNull(accessToken);
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
requireEmpty(request(provider.getInvalidationURL(), GSON.toJson(new RefreshRequest(accessToken, clientToken))));
}
/**
@@ -103,33 +80,49 @@ public class YggdrasilService {
*
* Game profile provided from authentication is not complete (no skin data in properties).
*
* @param uuid the uuid that the character corresponding to.
* @return the complete game profile(filled with more properties)
* @param userId the userId that the character corresponding to.
* @return the complete game profile(filled with more properties), null if character corresponding to {@code userId} does not exist.
* @throws AuthenticationException if an I/O error occurred or server response malformed.
*/
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid);
public GameProfile getCompleteGameProfile(UUID userId) throws AuthenticationException {
Objects.requireNonNull(userId);
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
ProfileResponse response = fromJson(request(provider.getProfilePropertiesURL(userId), null), ProfileResponse.class);
if (response == null)
return null;
return new GameProfile(response.getId(), response.getName(), response.getProperties());
}
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
Objects.requireNonNull(profile);
return Optional.ofNullable(profile.getProperties())
.flatMap(properties -> Optional.ofNullable(properties.get("textures")))
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), UTF_8))
.map(liftFunction(textures -> fromJson(textures, TextureResponse.class)))
.flatMap(response -> Optional.ofNullable(response.textures));
.map(properties -> properties.get("textures"))
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), Charsets.UTF_8))
.map(Lang.liftFunction(textures -> fromJson(textures, TextureResponse.class)))
.map(TextureResponse::getTextures);
}
private static YggdrasilSession handleAuthenticationResponse(String responseText, String clientToken) throws AuthenticationException {
private String request(URL url, Object input) throws AuthenticationException {
try {
if (input == null)
return NetworkUtils.doGet(url, proxy);
else
return NetworkUtils.doPost(url, input instanceof String ? (String) input : GSON.toJson(input), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
}
private static YggdrasilSession handle(String responseText, String clientToken) throws AuthenticationException {
AuthenticationResponse response = fromJson(responseText, AuthenticationResponse.class);
handleErrorMessage(response);
if (!clientToken.equals(response.clientToken))
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
if (!clientToken.equals(response.getClientToken()))
throw new AuthenticationException("Client token changed from " + response.getClientToken() + " to " + clientToken);
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
return new YggdrasilSession(response.getAccessToken(), response.getSelectedProfile(), response.getAvailableProfiles(), response.getUser());
}
private static void requireEmpty(String response) throws AuthenticationException {
@@ -144,30 +137,15 @@ 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"))
if (!StringUtils.isBlank(response.getError())) {
if (response.getErrorMessage() != null)
if (response.getErrorMessage().contains("Invalid credentials"))
throw new InvalidCredentialsException();
else if (response.errorMessage.contains("Invalid token"))
else if (response.getErrorMessage().contains("Invalid token"))
throw new InvalidTokenException();
else if (response.errorMessage.contains("Invalid username or password"))
else if (response.getErrorMessage().contains("Invalid username or password"))
throw new InvalidPasswordException();
}
throw new RemoteAuthenticationException(response.error, response.errorMessage, response.cause);
}
}
private String request(URL url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url, proxy);
else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
throw new RemoteAuthenticationException(response.getError(), response.getErrorMessage(), response.getCause());
}
}
@@ -179,27 +157,77 @@ public class YggdrasilService {
}
}
private class TextureResponse {
public Map<TextureType, Texture> textures;
}
private class AuthenticationResponse extends ErrorResponse {
public String accessToken;
public String clientToken;
public GameProfile selectedProfile;
public GameProfile[] availableProfiles;
public User user;
}
private class ErrorResponse {
public String error;
public String errorMessage;
public String cause;
}
private static final Gson GSON = new GsonBuilder()
static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(GameProfile.class, GameProfile.Serializer.INSTANCE)
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.create();
private static final class RefreshRequest {
private final String accessToken;
private final String clientToken;
private final GameProfile selectedProfile;
private final boolean requestUser;
public RefreshRequest(String accessToken, String clientToken) {
this(accessToken, clientToken, null);
}
public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile) {
this(accessToken, clientToken, selectedProfile, true);
}
public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile, boolean requestUser) {
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.requestUser = requestUser;
}
public String getAccessToken() {
return accessToken;
}
public String getClientToken() {
return clientToken;
}
public GameProfile getSelectedProfile() {
return selectedProfile;
}
public boolean isRequestUser() {
return requestUser;
}
}
private static final class ProfileResponse {
private final UUID id;
private final String name;
private final PropertyMap properties;
public ProfileResponse() {
this(UUID.randomUUID(), "", null);
}
public ProfileResponse(UUID id, String name, PropertyMap properties) {
this.id = id;
this.name = name;
this.properties = properties;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public PropertyMap getProperties() {
return properties;
}
}
}
@@ -1,52 +1,34 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilSession {
private String clientToken;
private String accessToken;
private final String accessToken;
private GameProfile selectedProfile;
private GameProfile[] availableProfiles;
private User user;
private final GameProfile[] availableProfiles;
private final User user;
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
this.clientToken = clientToken;
public YggdrasilSession(String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
this.accessToken = accessToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
this.user = user;
}
public String getClientToken() {
return clientToken;
}
public String getAccessToken() {
return accessToken;
}
/**
* @return nullable (null if no character is selected)
*/
public GameProfile getSelectedProfile() {
return selectedProfile;
}
/**
* @return nullable (null if the YggdrasilSession is loaded from storage)
*/
public GameProfile[] getAvailableProfiles() {
return availableProfiles;
}
@@ -55,40 +37,53 @@ public class YggdrasilSession {
return user;
}
public void setAvailableProfile(GameProfile profile) {
if (availableProfiles != null)
for (int i = 0; i < availableProfiles.length; ++i)
if (availableProfiles[i].getId().equals(profile.getId()))
availableProfiles[i] = profile;
if (selectedProfile != null && profile.getId().equals(selectedProfile.getId()))
selectedProfile = profile;
}
public void setSelectedProfile(GameProfile selectedProfile) {
this.selectedProfile = selectedProfile;
setAvailableProfile(selectedProfile);
}
public static YggdrasilSession fromStorage(Map<?, ?> storage) {
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
Optional<String> profileId = Lang.get(storage, "uuid", String.class);
Optional<String> profileName = Lang.get(storage, "displayName", String.class);
GameProfile profile = null;
if (profileId.isPresent() && profileName.isPresent()) {
profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get(),
Lang.get(storage, "profileProperties", Map.class).map(PropertyMap::fromMap).orElseGet(PropertyMap::new));
}
return new YggdrasilSession(
Lang.get(storage, "accessToken", String.class).orElse(null),
profile,
null,
Lang.get(storage, "userid", String.class)
.map(userId -> new User(userId, Lang.get(storage, "userProperties", Map.class).map(PropertyMap::fromMap).orElse(null)))
.orElse(null)
);
}
public Map<Object, Object> toStorage() {
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return mapOf(
pair("clientToken", clientToken),
pair("accessToken", accessToken),
pair("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId())),
pair("displayName", selectedProfile.getName()),
pair("userid", user.getId()),
pair("userProperties", user.getProperties()));
Map<Object, Object> storage = new HashMap<>();
storage.put("accessToken", accessToken);
if (selectedProfile != null) {
storage.put("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId()));
storage.put("displayName", selectedProfile.getName());
storage.put("profileProperties", selectedProfile.getProperties());
}
if (user != null) {
storage.put("userid", user.getId());
storage.put("userProperties", user.getProperties());
}
return storage;
}
public AuthInfo toAuthInfo() {
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken,
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
}
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
}
@@ -20,6 +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.LiteLoaderBMCLVersionList;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
@@ -33,11 +34,26 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
private BMCLAPIDownloadProvider() {
}
@Override
public String getLibraryBaseURL() {
return "https://bmclapi2.bangbang93.com/libraries/";
}
@Override
public String getVersionListURL() {
return "https://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
}
@Override
public String getVersionBaseURL() {
return "https://bmclapi2.bangbang93.com/versions/";
}
@Override
public String getAssetIndexBaseURL() {
return "https://bmclapi2.bangbang93.com/indexes/";
}
@Override
public String getAssetBaseURL() {
return "https://bmclapi2.bangbang93.com/assets/";
@@ -24,8 +24,14 @@ package org.jackhuang.hmcl.download;
*/
public interface DownloadProvider {
String getLibraryBaseURL();
String getVersionListURL();
String getVersionBaseURL();
String getAssetIndexBaseURL();
String getAssetBaseURL();
/**
@@ -28,11 +28,26 @@ import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
*/
public class MojangDownloadProvider implements DownloadProvider {
@Override
public String getLibraryBaseURL() {
return "https://libraries.minecraft.net/";
}
@Override
public String getVersionListURL() {
return "https://launchermeta.mojang.com/mc/game/version_manifest.json";
}
@Override
public String getVersionBaseURL() {
return "https://s3.amazonaws.com/Minecraft.Download/versions/";
}
@Override
public String getAssetIndexBaseURL() {
return "https://s3.amazonaws.com/Minecraft.Download/indexes/";
}
@Override
public String getAssetBaseURL() {
return "https://resources.download.minecraft.net/";
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.game.AssetObject;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -34,9 +35,6 @@ import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
/**
*
* @author huangyuhui
@@ -93,7 +91,7 @@ public final class GameAssetDownloadTask extends Task {
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)));
String sha1 = DigestUtils.sha1Hex(FileUtils.readBytes(file));
if (sha1.equals(assetObject.getHash())) {
++downloaded;
Logging.LOG.finest("File $file has been downloaded successfully, skipped downloading");
@@ -33,8 +33,6 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* This task is to extract all asset objects described in asset index json.
*
@@ -84,7 +82,7 @@ public final class GameAssetRefreshTask extends TaskResult<Collection<Pair<File,
if (Thread.interrupted())
throw new InterruptedException();
res.add(pair(dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject), assetObject));
res.add(new Pair<>(dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject), assetObject));
updateProgress(++progress, index.getObjects().size());
}
setResult(res);
@@ -22,13 +22,11 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Platform;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.VersionNumber;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
public class LibrariesUniqueTask extends TaskResult<Version> {
@@ -5,10 +5,7 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.*;
import java.io.*;
import java.nio.charset.Charset;
@@ -18,9 +15,6 @@ import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
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;
@@ -83,7 +77,7 @@ public final class LibraryDownloadTask extends Task {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
boolean valid = checksums.contains(DigestUtils.sha1Hex(fileData));
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
}
return validateJar(fileData, checksums);
@@ -104,7 +98,7 @@ public final class LibraryDownloadTask extends Task {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
}
if (!entry.isDirectory()) {
files.put(entry.getName(), encodeHex(digest("SHA-1", eData)));
files.put(entry.getName(), DigestUtils.sha1Hex(eData));
}
entry = jar.getNextJarEntry();
}
@@ -54,22 +54,28 @@ public final class Arguments {
return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm);
}
public Arguments addGameArguments(String... gameArguments) {
return addGameArguments(Arrays.asList(gameArguments));
public static Arguments addGameArguments(Arguments arguments, String... gameArguments) {
return addGameArguments(arguments, Arrays.asList(gameArguments));
}
public Arguments addGameArguments(List<String> gameArguments) {
public static Arguments addGameArguments(Arguments arguments, List<String> gameArguments) {
List<Argument> list = gameArguments.stream().map(StringArgument::new).collect(Collectors.toList());
return new Arguments(Lang.merge(getGame(), list), getJvm());
if (arguments == null)
return new Arguments(list, null);
else
return new Arguments(Lang.merge(arguments.getGame(), list), arguments.getJvm());
}
public Arguments addJVMArguments(String... jvmArguments) {
return addJVMArguments(Arrays.asList(jvmArguments));
public static Arguments addJVMArguments(Arguments arguments, String... jvmArguments) {
return addJVMArguments(arguments, Arrays.asList(jvmArguments));
}
public Arguments addJVMArguments(List<String> jvmArguments) {
public static Arguments addJVMArguments(Arguments arguments, List<String> jvmArguments) {
List<Argument> list = jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList());
return new Arguments(getGame(), Lang.merge(getJvm(), list));
if (arguments == null)
return new Arguments(null, list);
else
return new Arguments(arguments.getGame(), Lang.merge(arguments.getJvm(), list));
}
public static Arguments merge(Arguments a, Arguments b) {
@@ -30,7 +30,6 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Stream;
/**
* An implementation of classic Minecraft game repository.
@@ -155,26 +154,8 @@ public class DefaultGameRepository implements GameRepository {
File file = getVersionRoot(id);
if (!file.exists())
return true;
// test if no file in this version directory is occupied.
File removedFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + "_removed");
if (!file.renameTo(removedFile))
return false;
// 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);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
}
return true;
return FileUtils.deleteDirectoryQuietly(file);
}
protected void refreshVersionsImpl() {
@@ -189,49 +170,50 @@ public class DefaultGameRepository implements GameRepository {
File[] files = new File(getBaseDirectory(), "versions").listFiles();
if (files != null)
Arrays.stream(files).parallel().filter(File::isDirectory).flatMap(dir -> {
String id = dir.getName();
File json = new File(dir, id + ".json");
for (File dir : files)
if (dir.isDirectory()) {
String id = dir.getName();
File json = new File(dir, id + ".json");
// If user renamed the json file by mistake or created the json file in a wrong name,
// we will find the only json and rename it to correct name.
if (!json.exists()) {
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() == 1)
if (!jsons.get(0).renameTo(json)) {
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
return Stream.empty();
}
}
Version version;
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e) {
// JsonSyntaxException or IOException or NullPointerException(!!)
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
return Stream.empty();
// If user renamed the json file by mistake or created the json file in a wrong name,
// we will find the only json and rename it to correct name.
if (!json.exists()) {
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() == 1)
if (!jsons.get(0).renameTo(json)) {
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
continue;
}
}
Version version;
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e2) {
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
return Stream.empty();
}
}
if (!id.equals(version.getId())) {
version = version.setId(id);
try {
FileUtils.writeText(json, Constants.GSON.toJson(version));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
return Stream.empty();
}
}
// JsonSyntaxException or IOException or NullPointerException(!!)
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
continue;
return Stream.of(version);
}).forEachOrdered(provider::addVersion);
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e2) {
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
continue;
}
}
if (!id.equals(version.getId())) {
version = version.setId(id);
try {
FileUtils.writeText(json, Constants.GSON.toJson(version));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
continue;
}
}
provider.addVersion(version);
}
for (Version version : provider.getVersionMap().values()) {
try {
@@ -19,14 +19,13 @@ package org.jackhuang.hmcl.game;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.IOUtils;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* @author huangyuhui
*/
@@ -56,7 +55,7 @@ public final class GameVersion {
private static Optional<String> getVersionOfOldMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
byte[] bytes = "Minecraft Minecraft ".getBytes(US_ASCII);
byte[] bytes = "Minecraft Minecraft ".getBytes(Charsets.US_ASCII);
int j = matchArray(tmp, bytes);
if (j < 0)
return Optional.empty();
@@ -65,25 +64,25 @@ public final class GameVersion {
if ((j = lessThan32(tmp, i)) < 0)
return Optional.empty();
return Optional.of(new String(tmp, i, j - i, US_ASCII));
return Optional.of(new String(tmp, i, j - i, Charsets.US_ASCII));
}
private static Optional<String> getVersionOfNewMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
byte[] str = "-server.txt".getBytes(US_ASCII);
byte[] str = "-server.txt".getBytes(Charsets.US_ASCII);
int j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
int i = j + str.length;
i += 11;
j = lessThan32(tmp, i);
if (j < 0) return Optional.empty();
String result = new String(tmp, i, j - i, US_ASCII);
String result = new String(tmp, i, j - i, Charsets.US_ASCII);
char ch = result.charAt(0);
// 1.8.1+
if (ch < '0' || ch > '9') {
str = "Can't keep up! Did the system time change, or is the server overloaded?".getBytes(US_ASCII);
str = "Can't keep up! Did the system time change, or is the server overloaded?".getBytes(Charsets.US_ASCII);
j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
i = -1;
@@ -101,7 +100,7 @@ public final class GameVersion {
while (tmp[k] >= 48 && tmp[k] <= 57 || tmp[k] == (int) '-' || tmp[k] == (int) '.' || tmp[k] >= 97 && tmp[k] <= (int) 'z')
k--;
k++;
return Optional.of(new String(tmp, k, i - k + 1, US_ASCII));
return Optional.of(new String(tmp, k, i - k + 1, Charsets.US_ASCII));
}
return Optional.of(result);
}
@@ -27,9 +27,6 @@ import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
*
* @author huangyuhui
@@ -194,8 +191,8 @@ public class DefaultLauncher extends Launcher {
);
}
private final Map<String, Supplier<Boolean>> forbiddens = mapOf(
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
private final Map<String, Supplier<Boolean>> forbiddens = Lang.mapOf(
new Pair<String, Supplier<Boolean>>("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
);
protected Map<String, Supplier<Boolean>> getForbiddens() {
@@ -235,18 +232,18 @@ public class DefaultLauncher extends Launcher {
}
protected Map<String, String> getConfigurations() {
return mapOf(
pair("${auth_player_name}", authInfo.getUsername()),
pair("${auth_session}", authInfo.getAccessToken()),
pair("${auth_access_token}", authInfo.getAccessToken()),
pair("${auth_uuid}", UUIDTypeAdapter.fromUUID(authInfo.getUUID())),
pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
pair("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
pair("${version_type}", version.getType().getId()),
pair("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
pair("${user_type}", "mojang"),
pair("${assets_index_name}", version.getAssetIndex().getId()),
pair("${user_properties}", authInfo.getUserProperties())
return Lang.mapOf(
new Pair<>("${auth_player_name}", authInfo.getUsername()),
new Pair<>("${auth_session}", authInfo.getAccessToken()),
new Pair<>("${auth_access_token}", authInfo.getAccessToken()),
new Pair<>("${auth_uuid}", authInfo.getUserId()),
new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
new Pair<>("${version_type}", version.getType().getId()),
new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()),
new Pair<>("${assets_index_name}", version.getAssetIndex().getId()),
new Pair<>("${user_properties}", authInfo.getUserProperties())
);
}
@@ -273,7 +270,9 @@ public class DefaultLauncher extends Launcher {
}
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
if (listener != null)
if (listener == null)
startMonitors(p);
else
startMonitors(p, listener, daemon);
return p;
}
@@ -311,6 +310,11 @@ public class DefaultLauncher extends Launcher {
throw new PermissionException();
}
private void startMonitors(ManagedProcess managedProcess) {
managedProcess.addRelatedThread(Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream()), "stdout-pump", true));
managedProcess.addRelatedThread(Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream()), "stderr-pump", true));
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener) {
startMonitors(managedProcess, processListener, true);
}
@@ -21,6 +21,7 @@ import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
@@ -28,9 +29,6 @@ import java.io.FileInputStream;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class MinecraftInstanceTask<T> extends Task {
private final File zipFile;
@@ -64,7 +62,7 @@ public final class MinecraftInstanceTask<T> extends Task {
if (path.startsWith("/") || path.startsWith("\\"))
path = path.substring(1);
overrides.add(new ModpackConfiguration.FileInformation(path, encodeHex(digest("SHA-1", zip))));
overrides.add(new ModpackConfiguration.FileInformation(path, DigestUtils.sha1Hex(zip)));
}
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
@@ -30,9 +31,6 @@ import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public class ModpackInstallTask<T> extends Task {
private final File modpackFile;
@@ -94,8 +92,8 @@ public class ModpackInstallTask<T> extends Task {
byte[] data = os.toByteArray();
if (files.contains(path) && entryFile.exists()) {
String oldHash = encodeHex(digest("SHA-1", new FileInputStream(entryFile)));
String newHash = encodeHex(digest("SHA-1", new ByteArrayInputStream(data)));
String oldHash = DigestUtils.sha1Hex(new FileInputStream(entryFile));
String newHash = DigestUtils.sha1Hex(new ByteArrayInputStream(data));
if (!oldHash.equals(newHash)) {
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf);
@@ -109,7 +109,7 @@ public final class MultiMCModpackInstallTask extends Task {
version = version
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
.setMainClass(patch.getMainClass())
.setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments));
.setArguments(Arguments.addGameArguments(version.getArguments().orElse(null), newArguments));
}
}
}
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
@@ -35,8 +32,6 @@ import java.net.URL;
import java.security.MessageDigest;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
/**
* A task that can download a file online.
*
@@ -159,7 +154,7 @@ public class FileDownloadTask extends Task {
temp = FileUtils.createTempFile();
rFile = new RandomAccessFile(temp, "rw");
MessageDigest digest = getDigest("SHA-1");
MessageDigest digest = DigestUtils.getSha1Digest();
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -30,8 +31,6 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huang
@@ -53,7 +52,7 @@ public final class GetTask extends TaskResult<String> {
}
public GetTask(URL url, Proxy proxy, String id) {
this(url, proxy, id, UTF_8);
this(url, proxy, id, Charsets.DEFAULT_CHARSET);
}
public GetTask(URL url, Proxy proxy, String id, Charset charset) {
@@ -28,7 +28,7 @@ import java.util.Collection;
*/
public final class ParallelTask extends Task {
private final Collection<Task> tasks;
private final Task[] tasks;
/**
* Constructor.
@@ -36,11 +36,6 @@ public final class ParallelTask extends Task {
* @param tasks the tasks that can be executed parallelly.
*/
public ParallelTask(Task... tasks) {
this.tasks = Arrays.asList(tasks);
setSignificance(TaskSignificance.MINOR);
}
public ParallelTask(Collection<Task> tasks) {
this.tasks = tasks;
setSignificance(TaskSignificance.MINOR);
}
@@ -51,7 +46,7 @@ public final class ParallelTask extends Task {
@Override
public Collection<Task> getDependents() {
return tasks;
return Arrays.asList(tasks);
}
}
@@ -119,12 +119,12 @@ public final class Schedulers {
Logging.LOG.info("Shutting down executor services.");
if (CACHED_EXECUTOR != null)
CACHED_EXECUTOR.shutdownNow();
CACHED_EXECUTOR.shutdown();
if (IO_EXECUTOR != null)
IO_EXECUTOR.shutdownNow();
IO_EXECUTOR.shutdown();
if (SINGLE_EXECUTOR != null)
SINGLE_EXECUTOR.shutdownNow();
SINGLE_EXECUTOR.shutdown();
}
}
@@ -0,0 +1,55 @@
/*
* Hello Minecraft!.
* 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 java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
public final class Charsets {
private Charsets() {
}
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
public static final Charset US_ASCII = Charset.forName("US-ASCII");
public static final Charset UTF_16 = Charset.forName("UTF-16");
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
public static final Charset UTF_8 = Charset.forName("UTF-8");
public static final Charset DEFAULT_CHARSET = UTF_8;
public static Charset toCharset(String charset) {
if (charset == null)
return Charset.defaultCharset();
try {
return Charset.forName(charset);
} catch (UnsupportedCharsetException ignored) {
return Charset.defaultCharset();
}
}
public static Charset toCharset() {
return toCharset(System.getProperty("sun.jnu.encoding", Constants.DEFAULT_ENCODING));
}
}
@@ -43,6 +43,8 @@ public final class Constants {
private Constants() {
}
public static final String DEFAULT_ENCODING = "UTF-8";
public static final Charset DEFAULT_CHARSET = Charsets.UTF_8;
public static final Charset SYSTEM_CHARSET = Charset.forName(OperatingSystem.ENCODING);
public static final String DEFAULT_LIBRARY_URL = "https://libraries.minecraft.net/";
@@ -22,8 +22,6 @@ import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huangyuhui
@@ -35,6 +33,11 @@ public final class DigestUtils {
private static final int STREAM_BUFFER_LENGTH = 1024;
private static byte[] digest(MessageDigest digest, InputStream data)
throws IOException {
return updateDigest(digest, data).digest();
}
public static MessageDigest getDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
@@ -43,23 +46,193 @@ public final class DigestUtils {
}
}
public static byte[] digest(String algorithm, String data) {
return digest(algorithm, data.getBytes(UTF_8));
public static MessageDigest getMd2Digest() {
return getDigest("MD2");
}
public static byte[] digest(String algorithm, byte[] data) {
return getDigest(algorithm).digest(data);
public static MessageDigest getMd5Digest() {
return getDigest("MD5");
}
public static byte[] digest(String algorithm, InputStream data) throws IOException {
return digest(getDigest(algorithm), data);
public static MessageDigest getSha1Digest() {
return getDigest("SHA-1");
}
public static byte[] digest(MessageDigest digest, InputStream data) throws IOException {
return updateDigest(digest, data).digest();
public static MessageDigest getSha256Digest() {
return getDigest("SHA-256");
}
public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException {
public static MessageDigest getSha384Digest() {
return getDigest("SHA-384");
}
public static MessageDigest getSha512Digest() {
return getDigest("SHA-512");
}
public static byte[] md2(byte[] data) {
return getMd2Digest().digest(data);
}
public static byte[] md2(InputStream data)
throws IOException {
return digest(getMd2Digest(), data);
}
public static byte[] md2(String data) {
return md2(data.getBytes(Charsets.UTF_8));
}
public static String md2Hex(byte[] data) {
return Hex.encodeHexString(md2(data));
}
public static String md2Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(md2(data));
}
public static String md2Hex(String data) {
return Hex.encodeHexString(md2(data));
}
public static byte[] md5(byte[] data) {
return getMd5Digest().digest(data);
}
public static byte[] md5(InputStream data)
throws IOException {
return digest(getMd5Digest(), data);
}
public static byte[] md5(String data) {
return md5(data.getBytes(Charsets.UTF_8));
}
public static String md5Hex(byte[] data) {
return Hex.encodeHexString(md5(data));
}
public static String md5Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(md5(data));
}
public static String md5Hex(String data) {
return Hex.encodeHexString(md5(data));
}
public static byte[] sha1(byte[] data) {
return getSha1Digest().digest(data);
}
public static byte[] sha1(InputStream data)
throws IOException {
return digest(getSha1Digest(), data);
}
public static byte[] sha1(String data) {
return sha1(data.getBytes(Charsets.UTF_8));
}
public static String sha1Hex(byte[] data) {
return Hex.encodeHexString(sha1(data));
}
public static String sha1Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha1(data));
}
public static String sha1Hex(String data) {
return Hex.encodeHexString(sha1(data));
}
public static byte[] sha256(byte[] data) {
return getSha256Digest().digest(data);
}
public static byte[] sha256(InputStream data)
throws IOException {
return digest(getSha256Digest(), data);
}
public static byte[] sha256(String data) {
return sha256(data.getBytes(Charsets.UTF_8));
}
public static String sha256Hex(byte[] data) {
return Hex.encodeHexString(sha256(data));
}
public static String sha256Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha256(data));
}
public static String sha256Hex(String data) {
return Hex.encodeHexString(sha256(data));
}
public static byte[] sha384(byte[] data) {
return getSha384Digest().digest(data);
}
public static byte[] sha384(InputStream data)
throws IOException {
return digest(getSha384Digest(), data);
}
public static byte[] sha384(String data) {
return sha384(data.getBytes(Charsets.UTF_8));
}
public static String sha384Hex(byte[] data) {
return Hex.encodeHexString(sha384(data));
}
public static String sha384Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha384(data));
}
public static String sha384Hex(String data) {
return Hex.encodeHexString(sha384(data));
}
public static byte[] sha512(byte[] data) {
return getSha512Digest().digest(data);
}
public static byte[] sha512(InputStream data)
throws IOException {
return digest(getSha512Digest(), data);
}
public static byte[] sha512(String data) {
return sha512(data.getBytes(Charsets.UTF_8));
}
public static String sha512Hex(byte[] data) {
return Hex.encodeHexString(sha512(data));
}
public static String sha512Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha512(data));
}
public static String sha512Hex(String data) {
return Hex.encodeHexString(sha512(data));
}
public static MessageDigest updateDigest(MessageDigest messageDigest, byte[] valueToDigest) {
messageDigest.update(valueToDigest);
return messageDigest;
}
public static MessageDigest updateDigest(MessageDigest digest, InputStream data)
throws IOException {
byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
@@ -71,4 +244,8 @@ public final class DigestUtils {
return digest;
}
public static MessageDigest updateDigest(MessageDigest messageDigest, String valueToDigest) {
messageDigest.update(valueToDigest.getBytes(Charsets.UTF_8));
return messageDigest;
}
}
@@ -26,8 +26,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huang
@@ -46,7 +44,7 @@ public final class FileUtils {
}
public static String readText(File file) throws IOException {
return readText(file, UTF_8);
return readText(file, Charsets.DEFAULT_CHARSET);
}
public static String readText(File file, Charset charset) throws IOException {
@@ -62,7 +60,7 @@ public final class FileUtils {
}
public static void writeText(File file, String text) throws IOException {
writeText(file, text, UTF_8);
writeText(file, text, Charsets.DEFAULT_CHARSET);
}
public static void writeText(File file, String text, Charset charset) throws IOException {
@@ -17,18 +17,20 @@
*/
package org.jackhuang.hmcl.util;
import java.io.IOException;
import java.nio.charset.Charset;
public final class Hex {
private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
public static byte[] decodeHex(String str) throws IOException {
char[] data = str.toCharArray();
private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
private final Charset charset;
public static byte[] decodeHex(char[] data) throws Exception {
int len = data.length;
if ((len & 0x1) != 0)
throw new IOException("Odd number of characters.");
throw new Exception("Odd number of characters.");
byte[] out = new byte[len >> 1];
@@ -44,24 +46,83 @@ public final class Hex {
return out;
}
public static String encodeHex(byte[] data) {
public static char[] encodeHex(byte[] data) {
return encodeHex(data, true);
}
public static char[] encodeHex(byte[] data, boolean toLowerCase) {
return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
}
protected static char[] encodeHex(byte[] data, char[] toDigits) {
int l = data.length;
char[] out = new char[l << 1];
int i = 0;
for (int j = 0; i < l; i++) {
out[(j++)] = DIGITS_LOWER[((0xF0 & data[i]) >>> 4)];
out[(j++)] = DIGITS_LOWER[(0xF & data[i])];
out[(j++)] = toDigits[((0xF0 & data[i]) >>> 4)];
out[(j++)] = toDigits[(0xF & data[i])];
}
return new String(out);
return out;
}
private static int toDigit(char ch, int index) throws IOException {
public static String encodeHexString(byte[] data) {
return new String(encodeHex(data));
}
protected static int toDigit(char ch, int index) {
int digit = Character.digit(ch, 16);
if (digit == -1)
throw new IOException("Illegal hexadecimal character " + ch + " at index " + index);
throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
return digit;
}
private Hex() {}
public Hex() {
this(Charsets.DEFAULT_CHARSET);
}
public Hex(Charset charset) {
this.charset = charset;
}
public byte[] decode(byte[] array) throws Exception {
return decodeHex(new String(array, getCharset()).toCharArray());
}
public Object decode(Object object) throws Exception {
try {
char[] charArray = (object instanceof String) ? ((String) object).toCharArray() : (char[]) object;
return decodeHex(charArray);
} catch (ClassCastException e) {
throw new Exception(e.getMessage(), e);
}
}
public byte[] encode(byte[] array) {
return encodeHexString(array).getBytes(getCharset());
}
public Object encode(Object object)
throws Exception {
try {
byte[] byteArray = (object instanceof String) ? ((String) object).getBytes(getCharset()) : (byte[]) object;
return encodeHex(byteArray);
} catch (ClassCastException e) {
throw new Exception(e.getMessage(), e);
}
}
public Charset getCharset() {
return this.charset;
}
public String getCharsetName() {
return this.charset.name();
}
@Override
public String toString() {
return super.toString() + "[charsetName=" + this.charset + "]";
}
}
@@ -18,20 +18,10 @@
package org.jackhuang.hmcl.util;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Represents a Java installation.
@@ -79,7 +69,7 @@ public final class JavaVersion implements Serializable {
return version;
}
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
private static final Pattern REGEX = Pattern.compile("java version \"(?<version>(.*?))\"");
public static final int UNKNOWN = -1;
public static final int JAVA_5 = 50;
@@ -165,14 +155,10 @@ public final class JavaVersion implements Serializable {
Platform.PLATFORM
);
private static List<JavaVersion> JAVAS;
private static Map<String, JavaVersion> JAVAS;
private static final CountDownLatch LATCH = new CountDownLatch(1);
public static Optional<List<JavaVersion>> getJREsImmediately() {
return Optional.ofNullable(JAVAS);
}
public static List<JavaVersion> getJREs() throws InterruptedException {
public static Map<String, JavaVersion> getJREs() throws InterruptedException {
if (JAVAS != null)
return JAVAS;
LATCH.await();
@@ -182,14 +168,13 @@ public final class JavaVersion implements Serializable {
public static synchronized void initialize() throws IOException {
if (JAVAS != null)
throw new IllegalStateException("JavaVersions have already been initialized.");
HashMap<String, JavaVersion> temp = new HashMap<>();
temp.put(THIS_JAVA.getVersion(), THIS_JAVA);
List<JavaVersion> javaVersions;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
javaVersions = queryWindows();
break;
case LINUX:
javaVersions = queryLinux();
break;
case OSX:
javaVersions = queryMacintosh();
break;
@@ -197,44 +182,12 @@ public final class JavaVersion implements Serializable {
javaVersions = Collections.emptyList();
break;
}
boolean isCurrentJavaIncluded = false;
for (int i = 0; i < javaVersions.size(); i++) {
if (THIS_JAVA.getBinary().equals(javaVersions.get(i).getBinary())) {
javaVersions.set(i, THIS_JAVA);
isCurrentJavaIncluded = true;
break;
}
}
if (!isCurrentJavaIncluded) {
javaVersions.add(THIS_JAVA);
}
JAVAS = Collections.unmodifiableList(javaVersions);
for (JavaVersion v : javaVersions)
temp.put(v.getVersion(), v);
JAVAS = Collections.unmodifiableMap(temp);
LATCH.countDown();
}
private static List<JavaVersion> queryLinux() throws IOException {
Path jvmDir = Paths.get("/usr/lib/jvm");
if (Files.isDirectory(jvmDir)) {
return Files.list(jvmDir)
.filter(dir -> Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS))
.map(dir -> dir.resolve("bin/java"))
.filter(Files::isExecutable)
.flatMap(executable -> {
try {
return Stream.of(fromExecutable(executable.toFile()));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Couldn't determine java " + executable, e);
return Stream.empty();
}
})
.collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
private static List<JavaVersion> queryMacintosh() throws IOException {
LinkedList<JavaVersion> res = new LinkedList<>();
@@ -218,12 +218,12 @@ public final class Lang {
* @param <V> the type that {@code obj} is being cast to.
* @return {@code obj} in the type of {@code V}.
*/
public static <V> Optional<V> tryCast(Object obj, Class<V> clazz) {
if (clazz.isInstance(obj)) {
return Optional.of(clazz.cast(obj));
} else {
@SuppressWarnings("unchecked")
public static <V> Optional<V> cast(Object obj, Class<V> clazz) {
if (obj == null || !ReflectionHelper.isInstance(clazz, obj))
return Optional.empty();
}
else
return Optional.of((V) obj);
}
/**
@@ -240,6 +240,20 @@ public final class Lang {
return Optional.ofNullable(list.get(index));
}
/**
* Get the value to which the specific key is mapped,
* or {@code null} if {@code map} has no mapping for the key
* or the value is not in the type of {@code clazz}.
*
* @param map the map
* @param key the key for finding the associate value.
* @param <V> the type of values in {@code map}
* @return the value to which the specific key is mapped, or {@code null} if {@code map} has no mapping for the key or the type is not correct.
*/
public static <V> Optional<V> get(Map<?, ?> map, Object key, Class<V> clazz) {
return cast(map.get(key), clazz);
}
/**
* Join two collections into one list.
*
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
@@ -38,12 +37,12 @@ public final class Logging {
LOG = Logger.getLogger("HMCL");
}
public static void start(File logFolder) {
public static void start() {
LOG.setLevel(Level.FINER);
LOG.setUseParentHandlers(false);
try {
FileHandler fileHandler = new FileHandler(new File(logFolder, "hmcl.log").getAbsolutePath());
FileHandler fileHandler = new FileHandler("hmcl.log");
fileHandler.setLevel(Level.FINEST);
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
LOG.addHandler(fileHandler);
@@ -28,7 +28,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.StringUtils.*;
/**
@@ -87,7 +86,7 @@ public final class NetworkUtils {
}
public static String doPost(URL url, String post, String contentType, Proxy proxy) throws IOException {
byte[] bytes = post.getBytes(UTF_8);
byte[] bytes = post.getBytes(Charsets.UTF_8);
HttpURLConnection con = createConnection(url, proxy);
con.setRequestMethod("POST");
@@ -109,12 +108,12 @@ public final class NetworkUtils {
InputStream is = null;
try {
is = con.getInputStream();
return IOUtils.readFullyAsString(is, UTF_8);
return IOUtils.readFullyAsString(is, Charsets.UTF_8);
} catch (IOException e) {
IOUtils.closeQuietly(is);
is = con.getErrorStream();
if (is != null)
return IOUtils.readFullyAsString(is, UTF_8);
return IOUtils.readFullyAsString(is, Charsets.UTF_8);
throw e;
} finally {
IOUtils.closeQuietly(is);
@@ -20,14 +20,10 @@ package org.jackhuang.hmcl.util;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Optional;
/**
* Represents the operating system.
@@ -107,9 +103,11 @@ public enum OperatingSystem {
else
CURRENT_OS = UNKNOWN;
TOTAL_MEMORY = getTotalPhysicalMemorySize()
.map(bytes -> (int) (bytes / 1024 / 1024))
.orElse(1024);
Object bytes = ReflectionHelper.call(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize");
if (bytes instanceof Long)
TOTAL_MEMORY = (int) (((Long) bytes) / 1024 / 1024);
else
TOTAL_MEMORY = 1024;
SUGGESTED_MEMORY = (int) (Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128);
@@ -119,19 +117,6 @@ public enum OperatingSystem {
SYSTEM_ARCHITECTURE = arch;
}
private static Optional<Long> getTotalPhysicalMemorySize() {
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
try {
Object attribute = mBeanServer.getAttribute(new ObjectName("java.lang", "type", "OperatingSystem"), "TotalPhysicalMemorySize");
if (attribute instanceof Long) {
return Optional.of((Long) attribute);
}
} catch (JMException e) {
return Optional.empty();
}
return Optional.empty();
}
public static void setClipboard(String string) {
ClipboardContent c = new ClipboardContent();
c.putString(string);
@@ -143,19 +128,4 @@ public enum OperatingSystem {
System.runFinalization();
System.gc();
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
}
}
}
@@ -26,14 +26,9 @@ import java.util.Objects;
*/
public class Pair<K, V> implements Map.Entry<K, V> {
public static <K, V> Pair<K, V> pair(K key, V value) {
return new Pair<>(key, value);
}
private K key;
private V value;
@Deprecated
public Pair(K key, V value) {
this.key = key;
this.value = value;
@@ -17,12 +17,136 @@
*/
package org.jackhuang.hmcl.util;
import sun.misc.Unsafe;
import java.lang.reflect.*;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
/**
*
* @author huangyuhui
*/
public final class ReflectionHelper {
private static final Unsafe UNSAFE;
private static final long OBJECT_FIELD_OFFSET;
private static final Map<String, Class<?>> PRIMITIVES;
static {
try {
UNSAFE = (Unsafe) AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return theUnsafe.get(null);
});
OBJECT_FIELD_OFFSET = UNSAFE.objectFieldOffset(getField(AccessibleObject.class, "override"));
} catch (PrivilegedActionException ex) {
throw new AssertionError(ex);
}
PRIMITIVES = Lang.mapOf(
new Pair<>("byte", Byte.class),
new Pair<>("short", Short.class),
new Pair<>("int", Integer.class),
new Pair<>("long", Long.class),
new Pair<>("char", Character.class),
new Pair<>("float", Float.class),
new Pair<>("double", Double.class),
new Pair<>("boolean", Boolean.class)
);
}
private static void setAccessibleForcibly(AccessibleObject object) {
UNSAFE.putBoolean(object, OBJECT_FIELD_OFFSET, true);
}
public static Method getMethod(Object object, String name) {
return getMethod(object.getClass(), name);
}
public static Method getMethod(Class<?> clazz, String name) {
try {
Method m = clazz.getDeclaredMethod(name);
setAccessibleForcibly(m);
return m;
} catch (Exception e) {
return null;
}
}
public static Field getField(Object object, String name) {
return getField(object.getClass(), name);
}
public static Field getField(Class<?> clazz, String name) {
try {
Field f = clazz.getDeclaredField(name);
setAccessibleForcibly(f);
return f;
} catch (Exception e) {
return null;
}
}
public static Object call(Class<?> cls, String name, Object object, Object... args) {
try {
if (args.length == 0)
try {
return cls.getDeclaredField(name).get(object);
} catch (NoSuchFieldException ignored) {
}
if ("new".equals(name)) {
for (Constructor<?> c : cls.getDeclaredConstructors())
if (checkParameter(c, args))
return c.newInstance(args);
} else
return forMethod(cls, name, args).get().invoke(object, args);
throw new RuntimeException();
} catch (Exception e) {
throw new IllegalArgumentException("Cannot find '" + name + "' in class '" + cls.getName() + "'", e);
}
}
public static Object construct(Class<?> clazz, Object... args) {
return call(clazz, "new", null, args);
}
public static Object call(Object obj, String name, Object... args) {
return call(obj.getClass(), name, obj, args);
}
public static boolean checkParameter(Executable exec, Object... args) {
Class<?>[] cArgs = exec.getParameterTypes();
if (args.length == cArgs.length) {
for (int i = 0; i < args.length; ++i) {
Object arg = args[i];
if (arg != null ? !isInstance(cArgs[i], arg) : cArgs[i].isPrimitive())
return false;
}
setAccessibleForcibly(exec);
return true;
} else
return false;
}
public static boolean isInstance(Class<?> superClass, Object obj) {
return superClass.isInstance(obj) || PRIMITIVES.get(superClass.getName()) == obj.getClass();
}
public static Optional<Method> forMethod(Class<?> cls, String name, Object... args) {
return Arrays.stream(cls.getDeclaredMethods())
.filter(s -> name.equals(s.getName()))
.filter(s -> checkParameter(s, args))
.findFirst();
}
public static StackTraceElement getCaller() {
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
StackTraceElement caller = elements[2];
-1
View File
@@ -50,7 +50,6 @@ allprojects {
compile "com.google.code.gson:gson:2.8.2"
compile "org.apache.commons:commons-compress:1.15"
compile "org.tukaani:xz:1.2"
compile "org.hildan.fxgson:fx-gson:3.1.0"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
+1 -2
View File
@@ -1,6 +1,5 @@
#Wed Jun 06 15:09:11 CST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = 'HMCL3'
rootProject.name = 'HMCLKotlin'
include 'HMCL'
include 'HMCLCore'