Compare commits

..
21 Commits
Author SHA1 Message Date
huangyuhui e31120be21 Friendly prompt that it cannot download some libraries successfully. 2018-07-22 18:50:44 +08:00
huangyuhui 247f02d423 Friendly prompt to ServerResponseMalformedException 2018-07-22 18:22:28 +08:00
huangyuhui 3da0c1d8e3 Abandon Launch4j which will not find Java inst in PATH or JAVA_HOME 2018-07-22 18:15:39 +08:00
huangyuhui c830bbc703 reorder version in main page 2018-07-21 21:58:46 +08:00
huangyuhui 7b619fef1d Fix always using BMCLAPIDownloadProvider 2018-07-21 20:24:54 +08:00
huanghongxun bac265a077 Merge pull request #399 from yushijinhun/refactor-account
重构账户部分
2018-07-21 19:50:46 +08:00
huanghongxun 39df0f8d43 Merge branch 'javafx' into refactor-account 2018-07-21 19:50:16 +08:00
huangyuhui 6af7f916ee Add an option to disable common directory globally 2018-07-21 19:45:59 +08:00
yushijinhun ae64097edf Automatically select the newly added account 2018-07-21 12:34:12 +08:00
yushijinhun cb6d8a88f1 Fix the old account isn't replaced when adding a same account 2018-07-21 12:31:08 +08:00
huangyuhui 12838994ad Reconstruct MultiFileItem 2018-07-21 11:39:41 +08:00
yushijinhun d30758a2a8 Throw NPE when selected server is null 2018-07-21 09:57:47 +08:00
yushijinhun b820aad358 Use MappedObservableList in AuthlibInjectorServersPage 2018-07-20 20:26:27 +08:00
yushijinhun 170189c344 Refactor accounts 2018-07-20 20:26:27 +08:00
yushijinhun f9e9c9d38b Use LinkedHashMap for mapOf 2018-07-20 20:26:27 +08:00
yushijinhun 088af981df Turn properties in SettingsPage from local variables into fields 2018-07-20 20:26:27 +08:00
yushijinhun 357fef8672 Call initProxy() manually 2018-07-20 20:26:27 +08:00
yushijinhun c38848ea0e Rename fields in Accounts 2018-07-20 20:26:27 +08:00
yushijinhun be8cd13b08 Throw IAE when getAccountType() receives an unexpected param 2018-07-20 20:26:27 +08:00
yushijinhun 487816ab74 Use AccountFactory in cboType 2018-07-20 20:26:27 +08:00
yushijinhun bcd9f8240c Extract account factories to static fields 2018-07-20 20:26:27 +08:00
61 changed files with 1309 additions and 748 deletions

No files matched your search

Binary file not shown.
+22 -18
View File
@@ -5,10 +5,6 @@ import java.util.jar.JarFile
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
plugins {
id "edu.sc.seis.launch4j" version "2.4.4"
}
if (!hasProperty('mainClass')) {
ext.mainClass = 'org.jackhuang.hmcl.Main'
}
@@ -97,21 +93,29 @@ task makePackGZ(dependsOn: jar) doLast {
fileEx.append sha1Hex
}
launch4j {
mainClassName = mainClass
icon = "${projectDir}/icon.ico"
task makeExecutable(dependsOn: jar) doLast {
ext {
jar.classifier = ''
makeExecutableinjar = jar.archivePath
jar.classifier = ''
makeExecutableoutjar = jar.archivePath
jar.classifier = ''
}
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe")
def fos = new FileOutputStream(loc)
def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe'))
int read
def bytes = new byte[8192]
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
is = new FileInputStream(makeExecutableoutjar)
while((read = is.read(bytes)) != -1)
fos.write(bytes, 0, read)
is.close()
fos.close()
jvmOptions = ["-XX:MinHeapFreeRatio=5", "-XX:MaxHeapFreeRatio=15"]
version = mavenVersion
productName = "HMCL"
internalName = "HMCL-${mavenVersion}"
outfile = "HMCL-${mavenVersion}.exe"
outputDir = "libs"
copyright = "Copyright (C) 2018 huangyuhui <hmcl@huangyuhui.net>"
}
build.dependsOn makePackGZ
build.dependsOn createExe
build.dependsOn makeExecutable
@@ -1,57 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.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();
}
}
@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
import java.util.Collections;
/**
* This event gets fired when loading accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountLoadingEvent extends Event {
private final Collection<Account> accounts;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountLoadingEvent(Object source, Collection<Account> accounts) {
super(source);
this.accounts = Collections.unmodifiableCollection(accounts);
}
public Collection<Account> getAccounts() {
return accounts;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("accounts", accounts)
.toString();
}
}
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Scheduler;
import org.jackhuang.hmcl.task.Schedulers;
@@ -42,7 +42,7 @@ public final class AccountHelper {
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
public static void loadSkins() {
for (Account account : Settings.INSTANCE.getAccounts()) {
for (Account account : Accounts.getAccounts()) {
if (account instanceof YggdrasilAccount) {
new SkinLoadTask((YggdrasilAccount) account, false).start();
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
@@ -29,6 +30,7 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
@@ -59,7 +61,11 @@ public class HMCLGameDownloadTask extends Task {
public void execute() {
File jar = profile.getRepository().getVersionJar(version);
File cache = new File(CONFIG.getCommonDirectory(), "jars/" + gameVersion + ".jar");
// Force using common directory will not affect the behaviour that repository acts
// Since we always copy the downloaded jar to .minecraft/versions/<version>/
File cache = new File(Optional.ofNullable(Settings.INSTANCE.getCommonDirectory())
.orElse(Settings.getDefaultCommonDirectory()),
"jars/" + gameVersion + ".jar");
if (cache.exists())
try {
FileUtils.copyFile(cache, jar);
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
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.Logging;
@@ -51,17 +52,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
return profile;
}
private boolean useSelf(String version, String assetId) {
VersionSetting vs = profile.getVersionSetting(version);
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon();
private boolean useSelf(String assetId) {
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists();
}
@Override
public File getAssetDirectory(String version, String assetId) {
if (useSelf(version, assetId))
if (Settings.INSTANCE.isCommonDirectoryDisabled() || useSelf(assetId))
return super.getAssetDirectory(version, assetId);
else
return new File(CONFIG.getCommonDirectory(), "assets");
return new File(Settings.INSTANCE.getCommonDirectory(), "assets");
}
@Override
@@ -83,10 +83,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
public File getLibraryFile(Version version, Library lib) {
VersionSetting vs = profile.getVersionSetting(version.getId());
File self = super.getLibraryFile(version, lib);
if (self.exists() || vs.isNoCommon())
if (Settings.INSTANCE.isCommonDirectoryDisabled() || self.exists())
return self;
else
return new File(CONFIG.getCommonDirectory(), "libraries/" + lib.getPath());
return new File(Settings.INSTANCE.getCommonDirectory(), "libraries/" + lib.getPath());
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
@@ -190,6 +191,8 @@ public final class LauncherHelper {
message = i18n("launch.failed.creating_process") + ex.getLocalizedMessage();
} else if (ex instanceof NotDecompressingNativesException) {
message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage();
} else if (ex instanceof LibraryDownloadException) {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause());
} else {
message = StringUtils.getStackTrace(ex);
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -29,14 +29,27 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author huangyuhui
@@ -44,33 +57,160 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public final class Accounts {
private Accounts() {}
public static final String OFFLINE_ACCOUNT_KEY = "offline";
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
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(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
private static final String TYPE_OFFLINE = "offline";
private static final String TYPE_YGGDRASIL_ACCOUNT = "yggdrasil";
private static final String TYPE_AUTHLIB_INJECTOR = "authlibInjector";
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
else if (account instanceof YggdrasilAccount) return YGGDRASIL_ACCOUNT_KEY;
else return YGGDRASIL_ACCOUNT_KEY;
private static Map<String, AccountFactory<?>> type2factory = mapOf(
pair(TYPE_OFFLINE, FACTORY_OFFLINE),
pair(TYPE_YGGDRASIL_ACCOUNT, FACTORY_YGGDRASIL),
pair(TYPE_AUTHLIB_INJECTOR, FACTORY_AUTHLIB_INJECTOR));
private static String accountType(Account account) {
if (account instanceof OfflineAccount)
return TYPE_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return TYPE_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return TYPE_YGGDRASIL_ACCOUNT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
static String getAccountId(Account account) {
return getAccountId(account.getUsername(), account.getCharacter());
public static AccountFactory<?> getAccountFactory(Account account) {
return type2factory.get(accountType(account));
}
static String getAccountId(String username, String character) {
return username + ":" + character;
private static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListProperty<Account> accountsWrapper = new ReadOnlyListWrapper<>(accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
CONFIG.setSelectedAccount(selected == null ? "" : accountId(selected));
}
};
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
CONFIG.getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
})
.collect(toList()));
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#CONFIG}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
CONFIG.getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
});
initialized = true;
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(CONFIG.getSelectedAccount()))
.findFirst()
.orElse(null));
}
public static ObservableList<Account> getAccounts() {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper;
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
public static void setSelectedAccount(Account selectedAccount) {
Accounts.selectedAccount.set(selectedAccount);
}
public static ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
// ==== authlib-injector ====
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return CONFIG.getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
@@ -90,4 +230,34 @@ public final class Accounts {
return server;
});
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
*/
private static void removeDanglingAuthlibInjectorAccounts() {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> loginType2name = mapOf(
pair(Accounts.FACTORY_OFFLINE, i18n("account.methods.offline")),
pair(Accounts.FACTORY_YGGDRASIL, i18n("account.methods.yggdrasil")),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, i18n("account.methods.authlib_injector")));
public static String getAccountTypeName(AccountFactory<?> factory) {
return Optional.ofNullable(loginType2name.get(factory))
.orElseThrow(() -> new IllegalArgumentException("No corresponding login type name"));
}
public static String getAccountTypeName(Account account) {
return getAccountTypeName(getAccountFactory(account));
}
// ====
}
@@ -89,6 +89,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("bgpath")
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@SerializedName("commonpath")
private StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@@ -126,7 +129,7 @@ public final class Config implements Cloneable, Observable {
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty("");
@@ -146,6 +149,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
@@ -225,6 +231,18 @@ public final class Config implements Cloneable, Observable {
return backgroundImage;
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
public ObjectProperty<EnumCommonDirectory> commonDirTypeProperty() {
return commonDirType;
}
public void setCommonDirType(EnumCommonDirectory commonDirType) {
this.commonDirType.set(commonDirType);
}
public String getCommonDirectory() {
return commonDirectory.get();
}
@@ -361,8 +379,8 @@ public final class Config implements Cloneable, Observable {
return configurations;
}
public ObservableList<Map<Object, Object>> getAccounts() {
return accounts;
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
public String getSelectedAccount() {
@@ -24,8 +24,10 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.logging.Level;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
public final class ConfigHolder {
@@ -36,15 +38,23 @@ public final class ConfigHolder {
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final Config CONFIG = initSettings();
private static Config upgradeSettings(Config deserialized, Map rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
return deserialized;
}
private static Config initSettings() {
Config config = new Config();
if (Files.exists(CONFIG_PATH)) {
try {
Config deserialized = Config.fromJson(new String(Files.readAllBytes(CONFIG_PATH), UTF_8));
String json = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
Map raw = new Gson().fromJson(json, Map.class);
Config deserialized = Config.fromJson(json);
if (deserialized == null) {
LOG.finer("Settings file is empty, use the default settings.");
} else {
config = deserialized;
config = upgradeSettings(deserialized, raw);
}
LOG.finest("Initialized settings.");
} catch (IOException | JsonParseException e) {
@@ -0,0 +1,33 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 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.setting;
public enum EnumCommonDirectory {
/**
* same to game directory
*/
DISABLED,
/**
* %appdata%/.minecraft or ~/.minecraft
*/
DEFAULT,
/**
* user customized common directory.
*/
CUSTOM
}
@@ -36,7 +36,7 @@ public final class ProxyManager {
private ProxyManager() {
}
private static final ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
private static ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
() -> {
String host = CONFIG.getProxyHost();
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
@@ -59,11 +59,7 @@ public final class ProxyManager {
return proxyProperty;
}
static {
initProxy();
}
private static void initProxy() {
static void init() {
proxyProperty.addListener(observable -> updateSystemProxy());
updateSystemProxy();
@@ -17,99 +17,49 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.text.Font;
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.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class Settings {
public static final Settings INSTANCE = new Settings();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final boolean firstLaunch;
private InvalidationListener accountChangeListener =
source -> CONFIG.getAccounts().setAll(
accounts.values().stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
return storage;
})
.collect(toList()));
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
ProxyManager.getProxy(); // init ProxyManager
for (Iterator<Map<Object, Object>> iterator = CONFIG.getAccounts().iterator(); iterator.hasNext();) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
if (factory == null) {
LOG.warning("Unrecognized account type, removing: " + settings);
iterator.remove();
continue;
}
Account account;
try {
account = factory.fromStorage(settings);
} catch (Exception e) {
LOG.log(Level.WARNING, "Malformed account storage, removing: " + settings, e);
iterator.remove();
continue;
}
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
}
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
this.selectedAccount.set(accounts.get(CONFIG.getSelectedAccount()));
ProxyManager.init();
Accounts.init();
checkProfileMap();
save();
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> save());
}
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
CONFIG.addListener(source -> save());
}
private void save() {
LOG.info("Saving config");
ConfigHolder.saveConfig(CONFIG);
}
@@ -145,21 +95,25 @@ public class Settings {
CONFIG.setLogLines(logLines);
}
/****************************************
* AUTHLIB INJECTORS *
****************************************/
public boolean isCommonDirectoryDisabled() {
return CONFIG.getCommonDirType() == EnumCommonDirectory.DISABLED;
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
* Don't call this before {@link #migrateAuthlibInjectorServers()} is called, otherwise old data would be lost.
*/
private void removeDanglingAuthlibInjectorAccounts() {
accounts.values().stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(((AuthlibInjectorAccount) it).getServer()))
.collect(toList())
.forEach(this::deleteAccount);
public static String getDefaultCommonDirectory() {
return Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
}
public String getCommonDirectory() {
switch (CONFIG.getCommonDirType()) {
case DISABLED:
return null;
case DEFAULT:
return getDefaultCommonDirectory();
case CUSTOM:
return CONFIG.getCommonDirectory();
default:
return null;
}
}
/****************************************
@@ -177,86 +131,6 @@ public class Settings {
CONFIG.setDownloadType(index);
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
if (a == null || !accounts.containsKey(Accounts.getAccountId(a))) {
Account acc = accounts.values().stream().findAny().orElse(null);
set(acc);
return acc;
} else return a;
}
@Override
public void set(Account newValue) {
if (newValue == null || accounts.containsKey(Accounts.getAccountId(newValue))) {
super.set(newValue);
}
}
@Override
public void invalidated() {
super.invalidated();
CONFIG.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
}
};
public Account getSelectedAccount() {
return selectedAccount.get();
}
public ObjectProperty<Account> selectedAccountProperty() {
return selectedAccount;
}
public void setSelectedAccount(Account selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
account.addListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
return accounts.get(Accounts.getAccountId(name, character));
}
public Collection<Account> getAccounts() {
return Collections.unmodifiableCollection(accounts.values());
}
public void deleteAccount(String name, String character) {
Account removed = accounts.remove(Accounts.getAccountId(name, character));
if (removed != null) {
removed.removeListener(accountChangeListener);
accountChangeListener.invalidated(removed);
onAccountLoading();
selectedAccount.get();
}
}
public void deleteAccount(Account account) {
accounts.remove(Accounts.getAccountId(account));
account.removeListener(accountChangeListener);
accountChangeListener.invalidated(account);
onAccountLoading();
selectedAccount.get();
}
/****************************************
* PROFILES *
****************************************/
@@ -347,8 +221,4 @@ public class Settings {
EventBus.EVENT_BUS.fireEvent(new ProfileLoadingEvent(this, getProfiles()));
onProfileChanged();
}
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
}
@@ -81,7 +81,7 @@ public final class VersionSetting {
}
/**
* Java version or null if user customizes java directory.
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
@@ -91,6 +91,15 @@ public final class VersionSetting {
javaProperty.set(java);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setDefaultJavaPath(null);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
@@ -278,23 +287,6 @@ public final class VersionSetting {
notCheckGameProperty.set(notCheckGame);
}
private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false);
public ImmediateBooleanProperty noCommonProperty() {
return noCommonProperty;
}
/**
* True if HMCL does not find/download libraries in/to common path.
*/
public boolean isNoCommon() {
return noCommonProperty.get();
}
public void setNoCommon(boolean noCommon) {
noCommonProperty.set(noCommon);
}
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
public ImmediateBooleanProperty showLogsProperty() {
@@ -455,7 +447,7 @@ public final class VersionSetting {
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if ("Custom".equals(getJava())) {
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(new File(getJavaDir()));
} catch (IOException e) {
@@ -477,6 +469,11 @@ public final class VersionSetting {
} else throw new Error();
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
@@ -490,7 +487,6 @@ public final class VersionSetting {
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
noCommonProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -555,7 +551,6 @@ public final class VersionSetting {
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("noCommon", src.isNoCommon());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
@@ -593,7 +588,6 @@ public final class VersionSetting {
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false));
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)]);
@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
@@ -97,7 +97,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), 15, 15));
lblCharacter.setText(account.getCharacter());
lblType.setText(AddAccountPane.accountType(account));
lblType.setText(Accounts.getAccountTypeName(account));
lblEmail.setText(account.getUsername());
btnRefresh.setVisible(account instanceof YggdrasilAccount);
@@ -105,7 +105,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Accounts.getAccounts().remove(account);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}
@@ -21,7 +21,7 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
@@ -33,15 +33,12 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
@@ -51,24 +48,25 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
public class AddAccountPane extends StackPane {
@FXML private JFXTextField txtUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@@ -85,23 +83,31 @@ public class AddAccountPane extends StackPane {
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(i18n("account.methods.offline"), i18n("account.methods.yggdrasil"), i18n("account.methods.authlib_injector"));
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getAccountTypeName));
cboType.getSelectionModel().select(0);
ReadOnlyIntegerProperty loginTypeIdProperty = cboType.getSelectionModel().selectedIndexProperty();
ReadOnlyObjectProperty<AccountFactory<?>> loginType = cboType.getSelectionModel().selectedItemProperty();
txtPassword.visibleProperty().bind(loginTypeIdProperty.isNotEqualTo(0));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginTypeIdProperty.isEqualTo(2));
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
linkManageInjectorServers.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtUsername.validate() || (loginTypeIdProperty.get() != 0 && !txtPassword.validate()),
txtUsername.textProperty(), txtPassword.textProperty(), loginTypeIdProperty));
() -> !( // consider the opposite situation: input is valid
txtUsername.validate() &&
// invisible means the field is not needed, neither should it be validated
(!txtPassword.isVisible() || txtPassword.validate()) &&
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
),
txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
}
/**
@@ -113,47 +119,48 @@ public class AddAccountPane extends StackPane {
}
}
/**
* Gets the additional data that needs to be passed into {@link AccountFactory#create(CharacterSelector, String, String, Object)}.
*/
private Object getAuthAdditionalData() {
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
if (factory == Accounts.FACTORY_AUTHLIB_INJECTOR) {
return requireNonNull(cboServers.getSelectionModel().getSelectedItem(), "selected server cannot be null");
}
return null;
}
@FXML
private void onCreationAccept() {
if (btnAccept.isDisabled())
return;
String username = txtUsername.getText();
String password = txtPassword.getText();
Object addtionalData;
int type = cboType.getSelectionModel().getSelectedIndex();
AccountFactory<?> factory;
switch (type) {
case 0:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY);
addtionalData = null;
break;
case 1:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY);
addtionalData = null;
break;
case 2:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY);
Optional<AuthlibInjectorServer> server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem());
if (server.isPresent()) {
addtionalData = server.get();
} else {
lblCreationWarning.setText(i18n("account.failed.no_selected_server"));
return;
}
break;
default:
throw new Error();
}
acceptPane.showSpinner();
lblCreationWarning.setText("");
setDisable(true);
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData))
String username = txtUsername.getText();
String password = txtPassword.getText();
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
Object additionalData = getAuthAdditionalData();
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
.finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
Account account = variables.get("create_account");
int oldIndex = Accounts.getAccounts().indexOf(account);
if (oldIndex == -1) {
Accounts.getAccounts().add(account);
} else {
// adding an already-added account
// instead of discarding the new account, we first remove the existing one then add the new one
Accounts.getAccounts().remove(oldIndex);
Accounts.getAccounts().add(oldIndex, account);
}
// select the new account
Accounts.setSelectedAccount(account);
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}, exception -> {
@@ -256,6 +263,8 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return i18n("account.failed.connect_authentication_server");
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
@@ -272,11 +281,4 @@ public class AddAccountPane extends StackPane {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
public static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return i18n("account.methods.yggdrasil");
else throw new Error(i18n("account.methods.no_method") + ": " + account);
}
}
@@ -17,17 +17,18 @@
*/
package org.jackhuang.hmcl.ui;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.ui.FXUtils.smoothScrolling;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.MappedObservableList;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
@@ -42,26 +43,21 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML private VBox listPane;
@FXML private StackPane contentPane;
private InvalidationListener serversListener;
private ObservableList<AuthlibInjectorServerItem> serverItems;
public AuthlibInjectorServersPage() {
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serversListener = observable -> updateServersList();
CONFIG.getAuthlibInjectorServers().addListener(new WeakInvalidationListener(serversListener));
updateServersList();
serverItems = MappedObservableList.create(CONFIG.getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
}
private void updateServersList() {
listPane.getChildren().setAll(
CONFIG.getAuthlibInjectorServers().stream()
.map(server -> new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer())))
.collect(toList()));
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
private void onAdd() {
Controllers.dialog(new AddAuthlibInjectorServerPane());
@@ -69,6 +69,7 @@ import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.Lang;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@@ -227,7 +228,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
Bindings.createObjectBinding(
() -> {
Image image = null;
if (CONFIG.getBackgroundImageType() == EnumBackgroundImage.CUSTOM) {
if (CONFIG.getBackgroundImageType() == EnumBackgroundImage.CUSTOM && CONFIG.getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(CONFIG.getBackgroundImage()))
.orElse(null);
}
@@ -21,6 +21,12 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
@@ -49,21 +55,39 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.util.MappedObservableList;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static javafx.collections.FXCollections.singletonObservableList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
private final VBox profilePane = new VBox();
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
private ListProperty<RipplerContainer> accountItems = new SimpleListProperty<>();
private ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>() {
{
accountItems.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
Account selected = get();
accountItems.forEach(item -> item.setSelected(
getAccountFromItem(item)
.map(it -> it == selected)
.orElse(false)));
}
};
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
@@ -90,30 +114,91 @@ public final class LeftPaneController {
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
// ==== Accounts ====
// Missing account item
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
accountItems.bind(
new When(Accounts.accountsProperty().emptyProperty())
.then(singletonObservableList(missingAccountRippler))
.otherwise(MappedObservableList.create(Accounts.getAccounts(), this::createAccountItem)));
Bindings.bindContent(accountPane.getChildren(), accountItems);
selectedAccount.bindBidirectional(Accounts.selectedAccountProperty());
// ====
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(this::onRefreshedVersions);
}
FXUtils.onChangeAndOperate(Settings.INSTANCE.selectedAccountProperty(), this::onSelectedAccountChanged);
onAccountsLoading();
// ==== Accounts ====
private Optional<Account> getAccountFromItem(RipplerContainer accountItem) {
return Optional.ofNullable(accountItem.getProperties().get("account"))
.map(Account.class::cast);
}
private static String accountSubtitle(Account account) {
if (account instanceof OfflineAccount)
return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount)
return account.getUsername();
else
return "";
}
private RipplerContainer createAccountItem(Account account) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
RipplerContainer rippler = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
rippler.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
selectedAccount.set(account);
}
});
rippler.getProperties().put("account", account);
rippler.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else {
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
}
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(rippler, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
// update skin
if (account instanceof YggdrasilAccount) {
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
return rippler;
}
public void checkAccount() {
if (Accounts.getAccounts().isEmpty())
addNewAccount();
}
private void addNewAccount() {
Controllers.dialog(new AddAccountPane());
}
private void onSelectedAccountChanged(Account newAccount) {
Platform.runLater(() -> {
for (Node node : accountPane.getChildren()) {
if (node instanceof RipplerContainer && node.getProperties().get("account") instanceof Account) {
boolean current = Objects.equals(node.getProperties().get("account"), newAccount);
((RipplerContainer) node).setSelected(current);
}
}
});
}
// ====
private void onProfileChanged(ProfileChangedEvent event) {
Profile profile = event.getProfile();
@@ -143,70 +228,6 @@ public final class LeftPaneController {
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
private static String accountType(Account account) {
if (account instanceof OfflineAccount) return i18n("account.methods.offline");
else if (account instanceof YggdrasilAccount) return account.getUsername();
else throw new Error(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);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY)
Settings.INSTANCE.setSelectedAccount(account);
});
ripplerContainer.getProperties().put("account", account);
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
if (account instanceof YggdrasilAccount) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
list.add(ripplerContainer);
}
if (Settings.INSTANCE.getAccounts().isEmpty()) {
RipplerContainer container = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
container.setOnMouseClicked(e -> addNewAccount());
list.add(container);
}
Platform.runLater(() -> accountPane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
@@ -247,9 +268,4 @@ public final class LeftPaneController {
}
});
}
public void checkAccount() {
if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount();
}
}
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
@@ -48,6 +49,8 @@ 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.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.List;
@@ -132,13 +135,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
item.setVersionName(id);
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
});
item.setOnScriptButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
@@ -150,7 +153,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
File file = chooser.showSaveDialog(Controllers.getStage());
if (file != null)
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, file);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, file);
}
});
item.setOnSettingsButtonClicked(e -> {
@@ -214,10 +217,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
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)
if (Accounts.getSelectedAccount() == null)
Controllers.dialog(i18n("login.empty_username"));
else
LauncherHelper.INSTANCE.launch(profile, Settings.INSTANCE.getSelectedAccount(), id, null);
LauncherHelper.INSTANCE.launch(profile, Accounts.getSelectedAccount(), id, null);
}
});
File iconFile = repository.getVersionIcon(id);
@@ -233,6 +236,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -50,8 +51,10 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
public final class SettingsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("settings.launcher"));
@@ -73,7 +76,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private FontComboBox cboFont;
@FXML
private FileItem fileCommonLocation;
private MultiFileItem<EnumCommonDirectory> fileCommonLocation;
@FXML
private Label lblDisplay;
@FXML
@@ -85,7 +88,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private ScrollPane scroll;
@FXML
private MultiFileItem backgroundItem;
private MultiFileItem<EnumBackgroundImage> backgroundItem;
@FXML
private StackPane themeColorPickerContainer;
@FXML
@@ -101,7 +104,9 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private Pane proxyPane;
{
private ObjectProperty<Proxy.Type> selectedProxyType;
public SettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
FXUtils.smoothScrolling(scroll);
@@ -148,7 +153,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
ObjectProperty<Proxy.Type> selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
invalidated();
}
@@ -179,40 +184,33 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}));
// ====
fileCommonLocation.pathProperty().bindBidirectional(CONFIG.commonDirectoryProperty());
fileCommonLocation.loadChildren(Arrays.asList(
fileCommonLocation.createChildren(i18n("launcher.common_directory.disabled"), EnumCommonDirectory.DISABLED),
fileCommonLocation.createChildren(i18n("launcher.common_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(CONFIG.commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(CONFIG.commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.INSTANCE.getCommonDirectory())
.orElse(i18n("launcher.common_directory.disabled")),
CONFIG.commonDirectoryProperty(), CONFIG.commonDirTypeProperty()));
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
// background
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
));
backgroundItem.setCustomUserData(EnumBackgroundImage.CUSTOM);
backgroundItem.getTxtCustom().textProperty().bindBidirectional(CONFIG.backgroundImageProperty());
ObjectProperty<EnumBackgroundImage> backgroundType = new SimpleObjectProperty<EnumBackgroundImage>(EnumBackgroundImage.DEFAULT) {
{
invalidated();
}
@Override
protected void invalidated() {
backgroundItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == get())
.findFirst()
.ifPresent(it -> it.setSelected(true));
}
};
backgroundItem.getGroup().selectedToggleProperty().addListener((observable, oldValue, newValue) -> backgroundType.set((EnumBackgroundImage) newValue.getUserData()));
backgroundType.bindBidirectional(CONFIG.backgroundImageTypeProperty());
), EnumBackgroundImage.CUSTOM);
backgroundItem.customTextProperty().bindBidirectional(CONFIG.backgroundImageProperty());
backgroundItem.selectedDataProperty().bindBidirectional(CONFIG.backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
new When(backgroundType.isEqualTo(EnumBackgroundImage.DEFAULT))
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(CONFIG.backgroundImageProperty()));
// ====
// theme
// ==== Theme ====
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
@@ -224,6 +222,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
});
themeColorPickerContainer.getChildren().setAll(picker);
Platform.runLater(() -> JFXDepthManager.setDepth(picker, 0));
// ====
}
public String getTitle() {
@@ -71,7 +71,7 @@ public final class VersionItem extends StackPane {
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
FXUtils.setLimitHeight(this, 149);
content = new VBox();
{
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -43,6 +44,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
@@ -70,11 +72,10 @@ public final class VersionSettingsController {
@FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory;
@FXML private JFXToggleButton chkNoJVMArgs;
@FXML private JFXToggleButton chkNoCommon;
@FXML private JFXToggleButton chkNoGameCheck;
@FXML private MultiFileItem globalItem;
@FXML private MultiFileItem javaItem;
@FXML private MultiFileItem gameDirItem;
@FXML private MultiFileItem<Boolean> globalItem;
@FXML private MultiFileItem<JavaVersion> javaItem;
@FXML private MultiFileItem<EnumGameDirectory> gameDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private ImagePickerItem iconPickerItem;
@@ -94,9 +95,12 @@ public final class VersionSettingsController {
initializeSelectedJava();
});
javaItem.setSelectedData(null);
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
gameDirItem.loadChildren(Arrays.asList(
gameDirItem.createChildren(i18n("settings.advanced.game_dir.default"), EnumGameDirectory.ROOT_FOLDER),
gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), EnumGameDirectory.VERSION_FOLDER)
@@ -132,16 +136,19 @@ public final class VersionSettingsController {
FXUtils.unbindString(txtServerIP, lastVersionSetting.serverIpProperty());
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
FXUtils.unbindBoolean(chkNoGameCheck, lastVersionSetting.notCheckGameProperty());
FXUtils.unbindBoolean(chkNoCommon, lastVersionSetting.noCommonProperty());
FXUtils.unbindBoolean(chkNoJVMArgs, lastVersionSetting.noJVMArgsProperty());
FXUtils.unbindBoolean(chkShowLogs, lastVersionSetting.showLogsProperty());
FXUtils.unbindEnum(cboLauncherVisibility);
globalItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.usesGlobalProperty());
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
}
// unbind data fields
globalItem.setToggleSelectedListener(null);
javaItem.setToggleSelectedListener(null);
gameDirItem.setToggleSelectedListener(null);
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
@@ -157,19 +164,15 @@ public final class VersionSettingsController {
FXUtils.bindString(txtServerIP, versionSetting.serverIpProperty());
FXUtils.bindBoolean(chkFullscreen, versionSetting.fullscreenProperty());
FXUtils.bindBoolean(chkNoGameCheck, versionSetting.notCheckGameProperty());
FXUtils.bindBoolean(chkNoCommon, versionSetting.noCommonProperty());
FXUtils.bindBoolean(chkNoJVMArgs, versionSetting.noJVMArgsProperty());
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
versionSetting.setUsesCustomJavaDir();
} else {
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
}
});
@@ -177,13 +180,13 @@ public final class VersionSettingsController {
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
if (versionSetting.isUsesGlobal())
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.TRUE).findFirst().ifPresent(it -> it.setSelected(true));
else
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
globalItem.selectedDataProperty().bindBidirectional(versionSetting.usesGlobalProperty());
globalItem.subtitleProperty().bind(Bindings.createStringBinding(() -> i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special"),
versionSetting.usesGlobalProperty()));
globalItem.setToggleSelectedListener(newValue -> {
// do not call versionSettings.setUsesGlobal(true/false)
// because versionSettings can be the global one.
// global versionSettings.usesGlobal is always true.
if ((Boolean) newValue.getUserData())
profile.globalizeVersionSetting(versionId);
else
@@ -192,19 +195,9 @@ public final class VersionSettingsController {
Platform.runLater(() -> loadVersionSetting(profile, versionId));
});
versionSetting.usesGlobalProperty().setChangedListenerAndOperate(it ->
globalItem.setSubtitle(i18n(versionSetting.isUsesGlobal() ? "settings.type.global" : "settings.type.special")));
gameDirItem.getGroup().getToggles().stream()
.filter(it -> it.getUserData() == versionSetting.getGameDirType())
.findFirst().ifPresent(toggle -> toggle.setSelected(true));
gameDirItem.setCustomUserData(EnumGameDirectory.CUSTOM);
gameDirItem.setToggleSelectedListener(newValue -> versionSetting.setGameDirType((EnumGameDirectory) newValue.getUserData()));
versionSetting.gameDirProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
versionSetting.gameDirTypeProperty().setChangedListener(it -> initGameDirSubtitle(versionSetting));
initGameDirSubtitle(versionSetting);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
lastVersionSetting = versionSetting;
@@ -219,30 +212,14 @@ public final class VersionSettingsController {
return;
}
List<Toggle> toggles = javaItem.getGroup().getToggles();
if ("Custom".equals(lastVersionSetting.getJava())) {
toggles.stream()
.filter(javaItem::isCustomToggle)
.findFirst()
.get().setSelected(true);
if (lastVersionSetting.isUsesCustomJavaDir()) {
javaItem.setSelectedData(null);
} else {
JavaVersion selectedJava;
try {
selectedJava = lastVersionSetting.getJavaVersion();
javaItem.setSelectedData(lastVersionSetting.getJavaVersion());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
toggles.stream()
.filter(it -> it.getUserData() == selectedJava)
.findFirst()
.orElseGet( // fallback to select current java
() -> toggles.stream()
.filter(it -> it.getUserData() == JavaVersion.fromCurrentEnvironment())
.findFirst()
.get())
.setSelected(true);
;
}
}
@@ -253,10 +230,6 @@ public final class VersionSettingsController {
.map(JavaVersion::getBinary).map(File::getAbsolutePath).orElse("Invalid Java Directory"))));
}
private void initGameDirSubtitle(VersionSetting versionSetting) {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
@@ -21,10 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
@@ -46,12 +43,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
public class MultiFileItem extends ComponentList {
public class MultiFileItem<T> extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -64,6 +64,7 @@ public class MultiFileItem extends ComponentList {
private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked")
public MultiFileItem(@NamedArg(value = "hasCustom", defaultValue = "true") boolean hasCustom) {
this.hasCustom = hasCustom;
@@ -110,6 +111,20 @@ public class MultiFileItem extends ComponentList {
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == newValue)
.findFirst();
if (!selecting.isPresent()) {
selecting = group.getToggles().stream()
.filter(it -> it.getUserData() == getFallbackData())
.findFirst();
}
selecting.ifPresent(toggle -> toggle.setSelected(true));
});
}
@@ -117,11 +132,11 @@ public class MultiFileItem extends ComponentList {
return createChildren(title, null);
}
public Node createChildren(String title, Object userData) {
public Node createChildren(String title, T userData) {
return createChildren(title, "", userData);
}
public Node createChildren(String title, String subtitle, Object userData) {
public Node createChildren(String title, String subtitle, T userData) {
BorderPane pane = new BorderPane();
pane.setStyle("-fx-padding: 3;");
FXUtils.setLimitHeight(pane, 20);
@@ -146,12 +161,9 @@ public class MultiFileItem extends ComponentList {
pane.getChildren().add(custom);
}
public void onExploreJavaDir() {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(i18n(getChooserTitle()));
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
txtCustom.setText(selectedDir.getAbsolutePath());
public void loadChildren(Collection<Node> list, T customUserData) {
loadChildren(list);
setCustomUserData(customUserData);
}
public ToggleGroup getGroup() {
@@ -182,7 +194,7 @@ public class MultiFileItem extends ComponentList {
this.chooserTitle.set(chooserTitle);
}
public void setCustomUserData(Object userData) {
public void setCustomUserData(T userData) {
radioCustom.setUserData(userData);
}
@@ -225,4 +237,28 @@ public class MultiFileItem extends ComponentList {
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public T getSelectedData() {
return selectedData.get();
}
public ObjectProperty<T> selectedDataProperty() {
return selectedData;
}
public void setSelectedData(T selectedData) {
this.selectedData.set(selectedData);
}
public T getFallbackData() {
return fallbackData.get();
}
public ObjectProperty<T> fallbackDataProperty() {
return fallbackData;
}
public void setFallbackData(T fallbackData) {
this.fallbackData.set(fallbackData);
}
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
@@ -100,9 +101,10 @@ public final class InstallerWizardProvider implements WizardProvider {
@Override
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
DownloadProvider provider = profile.getDependency().getDownloadProvider();
switch (step) {
case 0:
return new AdditionalInstallersPage(this, controller, profile.getRepository(), BMCLAPIDownloadProvider.INSTANCE);
return new AdditionalInstallersPage(this, controller, profile.getRepository(), provider);
default:
throw new IllegalStateException();
}
@@ -28,7 +28,7 @@ import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -67,7 +67,7 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
txtModpackName.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.textProperty().addListener(e -> checkValidation());
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse(""));
txtModpackAuthor.setText(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
@@ -13,7 +13,7 @@
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="153">
</JFXMasonryPane>
</ScrollPane>
<VBox style="-fx-padding: 15;" spacing="15" pickOnBounds="false" alignment="BOTTOM_RIGHT">
@@ -31,7 +31,7 @@
</right>
</BorderPane>
<FileItem fx:id="fileCommonLocation" name="%launcher.common_directory" title="%launcher.common_directory.choose" tooltip="%settings.launcher.common_path.tooltip" />
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
<MultiFileItem fx:id="backgroundItem" title="%launcher.background" chooserTitle="%launcher.background.choose"
hasSubtitle="true" customText="%settings.custom" />
@@ -139,14 +139,6 @@
<JFXToggleButton fx:id="chkNoJVMArgs" size="7" FXUtils.limitHeight="10" />
</right>
</BorderPane>
<BorderPane>
<left>
<Label text="%settings.advanced.no_common"/>
</left>
<right>
<JFXToggleButton fx:id="chkNoCommon" size="7" FXUtils.limitHeight="10" />
</right>
</BorderPane>
<BorderPane>
<left>
<Label text="%settings.advanced.dont_check_game_completeness"/>
@@ -35,12 +35,12 @@ account.choose=Choose a character
account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.server_response_malformed=Invalid server response. Is this a valid authentication server?
account.injector.add=Add an authentication server
account.injector.manage=Manage authentication servers
account.injector.manage.title=Authentication servers
@@ -50,7 +50,6 @@ account.injector.server_url=Server URL
account.injector.server_name=Server Name
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.no_method=No login method
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=None
@@ -140,6 +139,7 @@ launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine cannot be created. Java arguments may have problems. You can enable the no args mode in the settings.
launch.failed.creating_process=Failed to create process, maybe your java path is wrong, please modify your java path.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.download_library=Unable to download library %s.
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.exited_abnormally=Game exited abnormally, please visit the log, or ask someone for help.
launch.state.dependencies=Dependencies
@@ -155,6 +155,8 @@ launcher.background.choose=Choose background path.
launcher.background.default=Default
launcher.common_directory=Common Directory
launcher.common_directory.choose=Choose common directory.
launcher.common_directory.default=Default
launcher.common_directory.disabled=Disabled
launcher.contact=Contact Us
launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
@@ -35,12 +35,12 @@ account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.server_response_malformed=這個服務器不是合法的登錄驗證服務器
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
@@ -50,7 +50,6 @@ account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.methods.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
@@ -140,6 +139,7 @@ launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
@@ -155,6 +155,8 @@ launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.common_directory.default=默認
launcher.common_directory.disabled=禁用
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
@@ -35,12 +35,12 @@ account.choose=选择一个角色
account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.server_response_malformed=这个服务器不是合法的登录验证服务器
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.manage.title=认证服务器
@@ -50,7 +50,6 @@ account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
account.methods.no_method=没有登入方式
account.methods.offline=离线模式
account.methods.yggdrasil=正版登录
account.missing=没有账户
@@ -140,6 +139,7 @@ launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.state.dependencies=正在处理游戏依赖
@@ -153,8 +153,10 @@ launcher=启动器
launcher.background=背景地址
launcher.background.choose=选择背景路径
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
launcher.common_directory=公共文件夹
launcher.common_directory=公共文件夹(统一存放游戏资源文件,减少游戏体积)
launcher.common_directory.choose=选择公共文件夹
launcher.common_directory.default=默认(%AppData%/.minecraft 或者 ~/.minecraft)
launcher.common_directory.disabled=禁用(总是使用游戏路径)
launcher.contact=联系我们
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误,请复制下列内容并通过 MCBBS、贴吧、GitHub 或 Minecraft Forum 反馈 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
@@ -110,4 +111,16 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return server;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), server.hashCode());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AuthlibInjectorAccount))
return false;
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
return super.equals(another) && server.equals(another.server);
}
}
@@ -105,4 +105,17 @@ public class OfflineAccount extends Account {
.append("uuid", uuid)
.toString();
}
@Override
public int hashCode() {
return username.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OfflineAccount))
return false;
OfflineAccount another = (OfflineAccount) obj;
return username.equals(another.username);
}
}
@@ -169,4 +169,16 @@ public class YggdrasilAccount extends Account {
return "YggdrasilAccount[username=" + getUsername() + "]";
}
@Override
public int hashCode() {
return characterUUID.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof YggdrasilAccount))
return false;
YggdrasilAccount another = (YggdrasilAccount) obj;
return characterUUID.equals(another.characterUUID);
}
}
@@ -0,0 +1,34 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 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.download.game;
import org.jackhuang.hmcl.game.Library;
public class LibraryDownloadException extends Exception {
private final Library library;
public LibraryDownloadException(Library library, Throwable cause) {
super("Unable to download library " + library, cause);
this.library = library;
}
public Library getLibrary() {
return library;
}
}
@@ -55,7 +55,7 @@ public final class LibraryDownloadTask extends Task {
@Override
public Collection<? extends Task> getDependents() {
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.emptySet();
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
}
@Override
@@ -65,20 +65,23 @@ public final class LibraryDownloadTask extends Task {
@Override
public void execute() throws Exception {
if (isDependentsSucceeded() && library.getChecksums() != null) {
if (!isDependentsSucceeded()) {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
}
if (library.getChecksums() != null) {
unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums()))
throw new IOException("Checksum failed for " + library);
downloaded = true;
}
}
@Override
public Collection<? extends Task> getDependencies() {
return downloaded ? Collections.emptySet() : Collections.singleton(task);
}
private static boolean checksumValid(File libPath, List<String> checksums) {
try {
if ((checksums == null) || (checksums.isEmpty())) {
@@ -41,7 +41,7 @@ public final class ModManager {
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
Consumer<File> puter = modFile -> Lang.ignoringException(() -> modCache.put(id, ModInfo.fromFile(modFile)));
Optional.ofNullable(modsDirectory.listFiles()).map(Arrays::stream).ifPresent(files -> files.forEach(modFile -> {
if (modFile.isDirectory() && VersionNumber.parseVersion(modFile.getName()) != null)
if (modFile.isDirectory() && VersionNumber.parseVersion(modFile.getName()).isPresent())
Optional.ofNullable(modFile.listFiles()).map(Arrays::stream).ifPresent(x -> x.forEach(puter));
else
puter.accept(modFile);
@@ -63,6 +63,16 @@ public abstract class Task {
this.state = state;
}
private Throwable lastException = null;
public Throwable getLastException() {
return lastException;
}
void setLastException(Throwable e) {
lastException = e;
}
/**
* The scheduler that decides how this task runs.
*/
@@ -190,6 +190,7 @@ public final class TaskExecutor {
} catch (SilentException | RejectedExecutionException e) {
// do nothing
} catch (Exception e) {
task.setLastException(e);
lastException = e;
variables.set(LAST_EXCEPTION_ID, e);
if (task.getSignificance().shouldLog()) {
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 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.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* If a version string contains '-', a {@link ComposedVersionNumber}
* will be generated.
*
* Formats like 1.7.10-OptiFine, 1.12.2-Forge
*
* @author huangyuhui
*/
public final class ComposedVersionNumber extends VersionNumber {
List<VersionNumber> composed;
public static boolean isComposedVersionNumber(String version) {
return version.contains("-");
}
ComposedVersionNumber(String version) {
composed = Arrays.stream(version.split("-"))
.map(VersionNumber::asVersion)
.collect(Collectors.toList());
}
@Override
public int hashCode() {
return Objects.hash(composed);
}
@Override
public String toString() {
return composed.stream().map(VersionNumber::toString).collect(Collectors.joining("-"));
}
}
@@ -17,7 +17,10 @@
*/
package org.jackhuang.hmcl.util;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -28,57 +31,37 @@ import java.util.stream.Collectors;
*/
public final class IntVersionNumber extends VersionNumber {
private final List<Integer> version;
final List<Integer> version;
IntVersionNumber(List<Integer> version) {
this.version = version;
public static boolean isIntVersionNumber(String version) {
return version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
&& !version.contains("..") && StringUtils.isNotBlank(version);
}
IntVersionNumber(String version) {
if (!isIntVersionNumber(version))
throw new IllegalArgumentException("The version " + version + " is malformed, only dots and digits are allowed.");
List<Integer> versions = Arrays.stream(version.split("\\."))
.map(Integer::parseInt)
.collect(Collectors.toList());
while (!versions.isEmpty() && versions.get(versions.size() - 1) == 0)
versions.remove(versions.size() - 1);
this.version = versions;
}
public int get(int index) {
return version.get(index);
}
@Override
public int compareTo(VersionNumber o) {
if (!(o instanceof IntVersionNumber))
return 0;
IntVersionNumber other = (IntVersionNumber) o;
int len = Math.max(this.version.size(), other.version.size());
for (int i = 0; i < len; ++i) {
int thisInt = Lang.get(this.version, i).orElse(0);
int otherInt = Lang.get(other.version, i).orElse(0);
if (thisInt != otherInt)
return Integer.compare(thisInt, otherInt);
}
return 0;
}
@Override
public int hashCode() {
int hash = 3;
for (int i : version)
hash = 83 * hash + i;
return hash;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof VersionNumber))
return false;
if (!(obj instanceof IntVersionNumber))
return true;
IntVersionNumber other = (IntVersionNumber) obj;
if (version.size() != other.version.size())
return false;
for (int i = 0; i < version.size(); ++i)
if (!version.get(i).equals(other.version.get(i)))
return false;
return true;
return Objects.hash(version);
}
@Override
public String toString() {
return String.join(".", version.stream().map(Object::toString).collect(Collectors.toList()));
return version.stream().map(Object::toString).collect(Collectors.joining("."));
}
}
@@ -41,7 +41,7 @@ public final class Lang {
*/
@SafeVarargs
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
HashMap<K, V> map = new HashMap<>();
Map<K, V> map = new LinkedHashMap<>();
for (Pair<K, V> pair : pairs)
map.put(pair.getKey(), pair.getValue());
return map;
@@ -0,0 +1,160 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import static java.util.stream.Collectors.toCollection;
import static javafx.collections.FXCollections.unmodifiableObservableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
/**
* @author yushijinhun
*/
public final class MappedObservableList {
private MappedObservableList() {
}
private static class ReferenceHolder implements InvalidationListener {
@SuppressWarnings("unused")
private Object ref;
ReferenceHolder(Object ref) {
this.ref = ref;
}
@Override
public void invalidated(Observable observable) {
// no-op
}
}
private static class MappedObservableListUpdater<T, U> implements ListChangeListener<T> {
private ObservableList<T> origin;
private ObservableList<U> target;
private Function<T, U> mapper;
// If we directly synchronize changes to target, each operation on target will cause a event to be fired.
// So we first write changes to buffer. After all the changes are processed, we use target.setAll to synchronize the changes.
private List<U> buffer;
MappedObservableListUpdater(ObservableList<T> origin, ObservableList<U> target, Function<T, U> mapper) {
this.origin = origin;
this.target = target;
this.mapper = mapper;
this.buffer = new ArrayList<>(target);
}
@Override
public void onChanged(Change<? extends T> change) {
// cache removed elements to reduce calls to mapper
Map<T, LinkedList<U>> cache = new IdentityHashMap<>();
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
if (change.wasPermutated()) {
@SuppressWarnings("unchecked")
U[] temp = (U[]) new Object[to - from];
for (int i = 0; i < temp.length; i++) {
temp[i] = buffer.get(from + i);
}
for (int idx = from; idx < to; idx++) {
buffer.set(change.getPermutation(idx), temp[idx - from]);
}
} else {
if (change.wasRemoved()) {
List<? extends T> originRemoved = change.getRemoved();
List<U> targetRemoved = buffer.subList(from, from + originRemoved.size());
for (int i = 0; i < targetRemoved.size(); i++) {
pushCache(cache, originRemoved.get(i), targetRemoved.get(i));
}
targetRemoved.clear();
}
if (change.wasAdded()) {
@SuppressWarnings("unchecked")
U[] toAdd = (U[]) new Object[to - from];
for (int i = 0; i < toAdd.length; i++) {
toAdd[i] = map(cache, origin.get(from + i));
}
buffer.addAll(from, Arrays.asList(toAdd));
}
}
}
target.setAll(buffer);
}
private void pushCache(Map<T, LinkedList<U>> cache, T key, U value) {
cache.computeIfAbsent(key, any -> new LinkedList<>())
.push(value);
}
private U map(Map<T, LinkedList<U>> cache, T key) {
LinkedList<U> stack = cache.get(key);
if (stack != null && !stack.isEmpty()) {
return stack.pop();
}
return mapper.apply(key);
}
}
/**
* This methods creates a mapping of {@code origin}, using {@code mapper} as the converter.
*
* If an item is added to {@code origin}, {@code mapper} will be invoked to create a corresponding item, which will also be added to the returned {@code ObservableList}.
* If an item is removed from {@code origin}, the corresponding item in the returned {@code ObservableList} will also be removed.
* If {@code origin} is permutated, the returned {@code ObservableList} will also be permutated in the same way.
*
* The returned {@code ObservableList} is unmodifiable.
*/
public static <T, U> ObservableList<U> create(ObservableList<T> origin, Function<T, U> mapper) {
// create a already-synchronized target ObservableList<U>
ObservableList<U> target = origin.stream()
.map(mapper)
.collect(toCollection(FXCollections::observableArrayList));
// then synchronize further changes to target
ListChangeListener<T> listener = new MappedObservableListUpdater<>(origin, target, mapper);
// let target hold a reference to listener to prevent listener being garbage-collected before target is garbage-collected
target.addListener(new ReferenceHolder(listener));
// let origin hold a weak reference to listener, so that target can be garbage-collected when it's no longer used
origin.addListener(new WeakListChangeListener<>(listener));
// ref graph:
// target ------> listener <-weak- origin
// <------ ------>
return unmodifiableObservableList(target);
}
}
@@ -34,14 +34,6 @@ public final class StringVersionNumber extends VersionNumber {
this.version = version;
}
@Override
public int compareTo(VersionNumber o) {
if (!(o instanceof StringVersionNumber))
return 0;
return version.compareTo(((StringVersionNumber) o).version);
}
public String getVersion() {
return version;
}
@@ -56,15 +48,4 @@ public final class StringVersionNumber extends VersionNumber {
return version;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof VersionNumber))
return false;
if (!(obj instanceof StringVersionNumber))
return true;
return version.equals(((StringVersionNumber) obj).version);
}
}
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
/**
* The formatted version number represents a version string.
@@ -29,52 +26,76 @@ import java.util.Optional;
*/
public abstract class VersionNumber implements Comparable<VersionNumber> {
/**
* @throws IllegalArgumentException if there are some characters excluding digits and dots.
* @param version version string in form x.x.x
* @return the int version number
*/
public static IntVersionNumber asIntVersionNumber(String version) {
if (version.chars().filter(ch -> ch != '.' && (ch < '0' || ch > '9')).count() > 0
|| version.contains("..") || StringUtils.isBlank(version))
throw new IllegalArgumentException("The version " + version + " is malformed, only dots and digits are allowed.");
String[] s = version.split("\\.");
ArrayList<Integer> versions = new ArrayList<>(s.length);
for (String value : s) versions.add(Integer.parseInt(value));
while (!versions.isEmpty() && versions.get(versions.size() - 1) == 0)
versions.remove(versions.size() - 1);
return new IntVersionNumber(Collections.unmodifiableList(versions));
}
public static StringVersionNumber asStringVersionNumber(String version) {
return new StringVersionNumber(version);
}
public static VersionNumber asVersion(String version) {
Objects.requireNonNull(version);
try {
return asIntVersionNumber(version);
} catch (IllegalArgumentException e) {
return asStringVersionNumber(version);
}
if (ComposedVersionNumber.isComposedVersionNumber(version))
return new ComposedVersionNumber(version);
else if (IntVersionNumber.isIntVersionNumber(version))
return new IntVersionNumber(version);
else
return new StringVersionNumber(version);
}
public static Optional<String> parseVersion(String str) {
if (str.chars().anyMatch(it -> it != '.' && (it < '0' || it > '9')) || StringUtils.isBlank(str))
if (IntVersionNumber.isIntVersionNumber(str))
return Optional.of(new IntVersionNumber(str).toString());
else
return Optional.empty();
String[] s = str.split("\\.");
for (String i : s)
if (StringUtils.isBlank(i))
return Optional.empty();
StringBuilder builder = new StringBuilder();
int last = s.length - 1;
for (int i = s.length - 1; i >= 0; --i)
if (Integer.parseInt(s[i]) == 0)
last = i;
for (int i = 0; i < last; ++i)
builder.append(s[i]).append(".");
return Optional.of(builder.append(s[last]).toString());
}
@Override
public int compareTo(VersionNumber o) {
return COMPARATOR.compare(this, o);
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
else return toString().equals(obj.toString());
}
private static <T extends Comparable<T>> int compareTo(List<T> a, List<T> b) {
int i;
for (i = 0; i < a.size() && i < b.size(); ++i) {
int res = a.get(i).compareTo(b.get(i));
if (res != 0)
return res;
}
if (i < a.size()) return 1;
else if (i < b.size()) return -1;
else return 0;
}
public static final Comparator<VersionNumber> COMPARATOR = new Comparator<VersionNumber>() {
@Override
public int compare(VersionNumber a, VersionNumber b) {
if (a == null || b == null)
return 0;
else {
if (a instanceof ComposedVersionNumber) {
if (b instanceof ComposedVersionNumber)
return compareTo(((ComposedVersionNumber) a).composed, ((ComposedVersionNumber) b).composed);
else
return compare(((ComposedVersionNumber) a).composed.get(0), b);
} else if (a instanceof IntVersionNumber) {
if (b instanceof ComposedVersionNumber)
return -compare(b, a);
else if (b instanceof IntVersionNumber)
return compareTo(((IntVersionNumber) a).version, ((IntVersionNumber) b).version);
else if (b instanceof StringVersionNumber)
return a.toString().compareTo(b.toString());
} else if (a instanceof StringVersionNumber) {
if (b instanceof ComposedVersionNumber)
return -compare(b, a);
else if (b instanceof StringVersionNumber)
return a.toString().compareTo(b.toString());
else if (b instanceof IntVersionNumber)
return a.toString().compareTo(b.toString());
}
throw new IllegalArgumentException("Unrecognized VersionNumber " + a + " and " + b);
}
}
};
}
+3
View File
@@ -0,0 +1,3 @@
Debug
Release
.vs
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
+174
View File
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>HMCL</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141_xp</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141_xp</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MinSpace</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="HMCL.h" />
<ClInclude Include="Resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="HMCL.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="HMCL.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="HMCL.ico" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="stdafx.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="targetver.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Resource.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="HMCL.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="stdafx.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="HMCL.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="HMCL.rc">
<Filter>资源文件</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="HMCL.ico">
<Filter>资源文件</Filter>
</Image>
</ItemGroup>
</Project>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2005
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HMCL", "HMCL\HMCL.vcxproj", "{672B1019-E741-4C0D-A986-627E2ACE157B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x64.ActiveCfg = Debug|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x64.Build.0 = Debug|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x86.ActiveCfg = Debug|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Debug|x86.Build.0 = Debug|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x64.ActiveCfg = Release|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x64.Build.0 = Release|x64
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x86.ActiveCfg = Release|Win32
{672B1019-E741-4C0D-A986-627E2ACE157B}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FE40055B-673D-42F5-8AE4-6DF2C87EB659}
EndGlobalSection
EndGlobal