Compare commits

..
24 Commits
Author SHA1 Message Date
huanghongxun ef6cfa785f Query cursemeta for removed mods 2018-11-26 00:40:53 +08:00
huanghongxun 2b792e73c9 Fix Java 9 requirement 2018-11-26 00:40:52 +08:00
huanghongxun 0f2922dab4 Update gradle 2018-11-26 00:40:51 +08:00
huanghongxun 45b55e2853 Fix not displaying crash report window when errored in Application.start 2018-11-26 00:40:50 +08:00
huanghongxun 9c7795687f Show installation succeeded when Curse modpack not finished completion 2018-11-26 00:40:49 +08:00
yushijinhun b273c80a02 Add AuthlibInjectorAccount.toString() 2018-11-24 20:43:41 +08:00
yushijinhun 8cdab4710b Add response text to exception message when authlib-injector meta is malformed 2018-11-24 20:33:46 +08:00
yushijinhun ea5b31d537 Allow specifying authlib-injector path 2018-11-24 20:32:22 +08:00
yushijinhun bb0c11fa12 Log authentication exceptions 2018-11-23 23:44:45 +08:00
yushijinhun 37186439cb Fetch authlib-injector metadata on startup 2018-11-23 23:29:41 +08:00
yushijinhun ab9f93d68e Make update checker thread daemon 2018-11-23 23:12:38 +08:00
yushijinhun ca6920e82a Fix preferredLoginType is not exported in modpack 2018-11-23 23:05:24 +08:00
yushijinhun d9db108a31 Use flag instead of accountId to store selected account
Mentioned in #488
2018-11-23 22:57:21 +08:00
yushijinhun 34b7e1d582 Support use authlib-injector offline 2018-11-23 22:01:52 +08:00
yushijinhun 4d73187376 Use @JsonAdapter 2018-11-23 21:49:50 +08:00
yushijinhun dcd0a42705 Refactor AuthlibInjectorServer 2018-11-23 21:34:54 +08:00
yushijinhun 1b40916046 Rewrite config properties 2018-11-23 21:16:05 +08:00
yushijinhun a4e4782f2f Change type from ExceptionalSupplier to AuthlibInjectorDownloader 2018-11-23 17:47:51 +08:00
yushijinhun 875b0c27ef Delete css if bss is generated 2018-11-23 17:08:17 +08:00
huanghongxun a38a1775a0 Update Forge crash detection. Closes #486 2018-11-18 22:09:48 +08:00
huanghongxun fd6ae986c3 Fixed #485 2018-11-13 21:20:14 +08:00
huanghongxun 74a87e65c8 Compile css to bss to speed up launching 2018-11-11 21:12:03 +08:00
yushijinhun a285d2c27d Parallel ca cert check 2018-11-11 11:04:19 +08:00
huanghongxun 44ac3bc900 Fix messy name of installed modpack game 2018-11-06 22:03:43 +08:00
48 changed files with 848 additions and 328 deletions

No files matched your search

+1 -1
View File
@@ -33,4 +33,4 @@ NVIDIA
.settings
# netbeans
.nb-gradle
.nb-gradle
+18
View File
@@ -110,6 +110,24 @@ def createExecutable(String suffix, String header) {
createChecksum(output)
}
processResources {
ext.convertToBSS = { String resource ->
exclude resource
doFirst {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
}
}
}
from "build/compiled-resources"
convertToBSS "assets/css/root.css"
convertToBSS "assets/css/blue.css"
}
task makePack(dependsOn: jar) {
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
doLast {
@@ -49,25 +49,28 @@ public final class Launcher extends Application {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
try {
ConfigHolder.init();
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
// runLater to ensure ConfigHolder.init() finished initialization
Platform.runLater(() -> {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UpdateChecker.init();
primaryStage.show();
});
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
// runLater to ensure ConfigHolder.init() finished initialization
Platform.runLater(() -> {
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UpdateChecker.init();
primaryStage.show();
});
}
public static void main(String[] args) {
@@ -31,6 +31,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -39,10 +40,13 @@ public final class Main {
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
// This environment check will take ~300ms
thread(() -> checkDSTRootCAX3(), "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
@@ -25,7 +25,6 @@ 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.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -227,8 +226,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
beingModpackVersions.remove(id);
}
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
@@ -63,6 +63,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
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;
@@ -145,8 +146,10 @@ public final class LauncherHelper {
try {
variables.set("account", account.logIn());
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired: " + e);
variables.set("account", DialogController.logIn(account));
} catch (AuthenticationException e) {
LOG.warning("Authentication failed, try playing offline: " + e);
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
@@ -369,10 +372,12 @@ public final class LauncherHelper {
flag = true;
}
// Forge 2760 and above will crash game with LiteLoader.
// Forge 2760~2773 will crash game with LiteLoader.
if (!flag) {
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
.anyMatch(it -> VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2760") >= 0);
.anyMatch(it ->
0 <= VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) &&
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.dialog(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
@@ -29,8 +29,10 @@ import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
@@ -39,6 +41,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@@ -61,9 +64,7 @@ public final class Accounts {
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(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider)::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -98,10 +99,6 @@ public final class Accounts {
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
@@ -136,7 +133,7 @@ public final class Accounts {
// selection is valid, store it
if (!initialized)
return;
config().setSelectedAccount(selected == null ? "" : accountId(selected));
updateAccountStorages();
}
};
@@ -149,9 +146,12 @@ public final class Accounts {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
static Map<Object, Object> getAccountStorage(Account account) {
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@@ -186,27 +186,38 @@ public final class Accounts {
return;
}
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
}
});
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
Account selected = accounts.stream()
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null);
selectedAccount.set(selected);
Schedulers.io().schedule(() -> {
if (selected != null)
Account selected = selectedAccount.get();
if (selected != null) {
Schedulers.io().schedule(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
}
});
});
}
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
Schedulers.io().schedule(() -> {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
}
public static ObservableList<Account> getAccounts() {
@@ -230,21 +241,22 @@ public final class Accounts {
}
// ==== authlib-injector ====
private static AuthlibInjectorArtifactProvider createAuthlibInjectorArtifactProvider() {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(Metadata.HMCL_DIRECTORY, DownloadProviders::getDownloadProvider);
} else {
LOG.info("Using specified authlib-injector: " + authlibinjectorLocation);
return new SimpleAuthlibInjectorArtifactProvider(Paths.get(authlibinjectorLocation));
}
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return config().getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
// this usually happens when migrating data from an older version
AuthlibInjectorServer server;
try {
server = AuthlibInjectorServer.fetchServerInfo(url);
LOG.info("Migrated authlib injector server " + server);
} catch (IOException e) {
server = new AuthlibInjectorServer(url, url);
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
config().getAuthlibInjectorServers().add(server);
return server;
});
@@ -40,38 +40,32 @@ import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import java.io.File;
import java.lang.reflect.Modifier;
import java.net.Proxy;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
.registerTypeAdapter(SupportedLocale.class, new SupportedLocale.TypeAdapter())
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.create();
public static Config fromJson(String json) throws JsonParseException {
Config instance = CONFIG_GSON.fromJson(json, Config.class);
// Gson will replace the property fields (even they are final!)
// So we have to add the listeners again after deserialization
instance.addListenerToProperties();
Config loaded = CONFIG_GSON.fromJson(json, Config.class);
Config instance = new Config();
PropertyUtils.copyProperties(loaded, instance);
return instance;
}
@@ -126,9 +120,6 @@ public final class Config implements Cloneable, Observable {
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
@@ -139,7 +130,7 @@ public final class Config implements Cloneable, Observable {
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@@ -166,24 +157,7 @@ public final class Config implements Cloneable, Observable {
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
addListenerToProperties();
}
private void addListenerToProperties() {
Stream.of(getClass().getDeclaredFields())
.filter(it -> {
int modifiers = it.getModifiers();
return !Modifier.isTransient(modifiers) && !Modifier.isStatic(modifiers);
})
.filter(it -> Observable.class.isAssignableFrom(it.getType()))
.map(it -> {
try {
return (Observable) it.get(this);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to get my own properties");
}
})
.forEach(helper::receiveUpdatesFrom);
PropertyUtils.attachListener(this, helper);
}
@Override
@@ -394,18 +368,6 @@ public final class Config implements Cloneable, Observable {
return accountStorages;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public String getFontFamily() {
return fontFamily.get();
}
@@ -19,6 +19,10 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
@@ -27,10 +31,6 @@ import java.nio.file.Paths;
import java.util.Map;
import java.util.logging.Level;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -99,7 +99,7 @@ public final class ConfigHolder {
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
String content = new String(Files.readAllBytes(configLocation), UTF_8);
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -17,13 +17,8 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.LinkedList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -67,20 +62,21 @@ final class ConfigUpgrader {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
List<OfflineAccount> accounts = new LinkedList<>();
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
accounts.add(account);
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
});
});
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
if (!rawJson.containsKey("selectedAccount"))
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
deserialized.setSelectedAccount(Accounts.accountId(account)));
});
});
});
}
@@ -112,5 +108,22 @@ final class ConfigUpgrader {
}
});
}
tryCast(rawJson.get("selectedAccount"), String.class)
.ifPresent(selected -> {
deserialized.getAccountStorages().stream()
.filter(storage -> {
Object type = storage.get("type");
if ("offline".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("username"));
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
} else {
return false;
}
})
.findFirst()
.ifPresent(storage -> storage.put("selected", true));
});
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
@@ -47,6 +48,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
*
* @author huangyuhui
*/
@JsonAdapter(Profile.Serializer.class)
public final class Profile implements Observable {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final HMCLGameRepository repository;
@@ -217,11 +219,6 @@ public final class Profile implements Observable {
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null)
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
@@ -34,6 +35,7 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
@@ -82,14 +84,14 @@ public class Theme {
.replace("%base-color%", color)
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
css = "/assets/css/blue.css";
}
return new String[]{
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
"/assets/css/root.css"
};
}
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
@@ -43,6 +45,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting {
public transient String id;
@@ -550,11 +553,6 @@ public final class VersionSetting {
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) return JsonNull.INSTANCE;
@@ -1,18 +1,21 @@
package org.jackhuang.hmcl.ui.download;
import javafx.application.Platform;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -54,7 +57,22 @@ public class ModpackInstallWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
settings.put("failure_callback", new FailureCallback() {
@Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof CurseCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
} else {
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
}
} else if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
}
}
});
return finishModpackInstallingAsync(settings);
}
@@ -22,11 +22,14 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Map;
@@ -67,7 +70,16 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
@Override
public Object finish(Map<String, Object> settings) {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
settings.put("failure_callback", new FailureCallback() {
@Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
}
}
});
return finishVersionDownloadingAsync(settings);
}
@@ -81,11 +81,14 @@ public final class ExportWizardProvider implements WizardProvider {
dependency = dependency.then(Task.of(() -> {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
@@ -74,7 +74,9 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
if (settings.get("failure_callback") instanceof WizardProvider.FailureCallback)
((WizardProvider.FailureCallback)settings.get("failure_callback")).onFail(settings, executor.getLastException(), () -> onEnd());
else if (settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> onEnd());
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> onEnd());
@@ -26,4 +26,8 @@ public interface WizardProvider {
Object finish(Map<String, Object> settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
boolean cancel();
interface FailureCallback {
void onFail(Map<String, Object> settings, Exception exception, Runnable next);
}
}
@@ -128,7 +128,7 @@ public final class UpdateChecker {
}
}
});
});
}, "Update Checker", true);
});
}
}
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
@@ -45,7 +46,6 @@ import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -242,7 +242,7 @@ public final class UpdateHandler {
Path hmclVersionJson = Metadata.HMCL_DIRECTORY.resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
try {
Map<?, ?> content = new Gson().fromJson(new String(Files.readAllBytes(hmclVersionJson), UTF_8), Map.class);
Map<?, ?> content = new Gson().fromJson(FileUtils.readText(hmclVersionJson), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.util.Lang;
@@ -83,6 +84,7 @@ public final class Locales {
else throw new IllegalArgumentException("Unknown locale: " + locale);
}
@JsonAdapter(SupportedLocale.TypeAdapter.class)
public static class SupportedLocale {
private final Locale locale;
private final String name;
+1 -1
View File
@@ -1240,7 +1240,7 @@
}
.tree-table-row-cell .jfx-text-field {
-fx-focus-color: rgba(82, 100, 174);
-fx-focus-color: rgb(82, 100, 174);
}
.tree-table-row-cell .jfx-text-field:error {
@@ -120,6 +120,7 @@ input.url=Must be a valid URL.
install=Install New Game
install.failed=Failed to install
install.failed.downloading=Failed to install due to some files not downloaded successfully
install.installer.choose=Choose a %s version
install.installer.forge=Forge
install.installer.game=Game
@@ -223,6 +224,7 @@ modpack.task.install.error=This modpack file cannot be recognized. Only Curse, M
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install relative files to Curse modpack
modpack.type.curse.tolerable_error=But we cannot complete downloading all files of this Curse modpack. You can retry downloading when launching corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are deleted and cannot be downloaded. Please consider the latest version or other modpacks.
modpack.type.hmcl=HMCL
@@ -120,6 +120,7 @@ input.url=必須為有效連結
install=新增遊戲
install.failed=安裝失敗
install.failed.downloading=安裝失敗,部分文件未能完成下載
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
@@ -223,6 +224,7 @@ modpack.task.install.error=無法識別該整合包,目前僅支持導入 Curs
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下載,您可以在啟動該遊戲版本時繼續 Curse 整合包文件的下載。由於網路問題,您可能需要重試多次。
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該整合包的最新版本或者安裝其他整合包。
modpack.type.hmcl=HMCL
@@ -120,6 +120,7 @@ input.url=必须是合法的链接
install=添加游戏
install.failed=安装失败
install.failed.downloading=安装失败,部分文件未能完成下载
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
@@ -223,6 +224,7 @@ modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curs
modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.hmcl=HMCL
@@ -25,24 +25,24 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private AuthlibInjectorServer server;
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
private AuthlibInjectorArtifactProvider downloader;
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) {
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
this.authlibInjectorDownloader = authlibInjectorDownloader;
this.downloader = downloader;
this.server = server;
}
@@ -56,10 +56,23 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return inject(() -> super.logInWithPassword(password, selector));
}
@Override
public Optional<AuthInfo> playOffline() {
Optional<AuthInfo> auth = super.playOffline();
Optional<AuthlibInjectorArtifactInfo> artifact = downloader.getArtifactInfoImmediately();
Optional<String> prefetchedMeta = server.getMetadataResponse();
if (auth.isPresent() && artifact.isPresent() && prefetchedMeta.isPresent()) {
return Optional.of(auth.get().withArguments(generateArguments(artifact.get(), server, prefetchedMeta.get())));
} else {
return Optional.empty();
}
}
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
CompletableFuture<byte[]> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
try (InputStream in = new URL(server.getUrl()).openStream()) {
return readFullyWithoutClosing(in);
CompletableFuture<String> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
try {
return server.fetchMetadataResponse();
} catch (IOException e) {
throw new CompletionException(new ServerDisconnectException(e));
}
@@ -67,14 +80,14 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return authlibInjectorDownloader.get();
return downloader.getArtifactInfo();
} catch (IOException e) {
throw new CompletionException(new AuthlibInjectorDownloadException(e));
}
});
AuthInfo auth = loginAction.get();
byte[] prefetchedMeta;
String prefetchedMeta;
AuthlibInjectorArtifactInfo artifact;
try {
@@ -91,10 +104,14 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
}
return auth.withArguments(new Arguments().addJVMArguments(
return auth.withArguments(generateArguments(artifact, server, prefetchedMeta));
}
private static Arguments generateArguments(AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta)));
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
}
@Override
@@ -120,4 +137,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
return super.equals(another) && server.equals(another.server);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("username", getUsername())
.append("server", getServer())
.toString();
}
}
@@ -22,9 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@@ -32,14 +29,14 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
private AuthlibInjectorArtifactProvider downloader;
private Function<String, AuthlibInjectorServer> serverLookup;
/**
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
*/
public AuthlibInjectorAccountFactory(ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, Function<String, AuthlibInjectorServer> serverLookup) {
this.authlibInjectorDownloader = authlibInjectorDownloader;
public AuthlibInjectorAccountFactory(AuthlibInjectorArtifactProvider downloader, Function<String, AuthlibInjectorServer> serverLookup) {
this.downloader = downloader;
this.serverLookup = serverLookup;
}
@@ -52,7 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
server, authlibInjectorDownloader, username, null, null);
server, downloader, username, null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -71,6 +68,6 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
server, authlibInjectorDownloader, username, session.getSelectedProfile().getId(), session);
server, downloader, username, session.getSelectedProfile().getId(), session);
}
}
@@ -17,10 +17,39 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
public class AuthlibInjectorArtifactInfo {
public static AuthlibInjectorArtifactInfo from(Path location) throws IOException {
try (JarFile jarFile = new JarFile(location.toFile())) {
Attributes attributes = jarFile.getManifest().getMainAttributes();
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
if (!"authlib-injector".equals(title)) {
throw new IOException("Bad Implementation-Title");
}
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
int buildNumber;
try {
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
.map(Integer::parseInt)
.orElseThrow(() -> new IOException("Missing Build-Number"));
} catch (NumberFormatException e) {
throw new IOException("Bad Build-Number", e);
}
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
}
}
private int buildNumber;
private String version;
private Path location;
@@ -0,0 +1,29 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import java.io.IOException;
import java.util.Optional;
public interface AuthlibInjectorArtifactProvider {
AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException;
Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately();
}
@@ -32,13 +32,11 @@ import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AuthlibInjectorDownloader {
public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvider {
private static final String LATEST_BUILD_URL = "https://authlib-injector.yushi.moe/artifact/latest.json";
@@ -58,6 +56,7 @@ public class AuthlibInjectorDownloader {
this.downloadProvider = downloadProvider;
}
@Override
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
synchronized (artifactLocation) {
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
@@ -79,6 +78,11 @@ public class AuthlibInjectorDownloader {
}
}
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
return getLocalArtifact();
}
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
LOG.info("Checking update of authlib-injector");
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
@@ -116,38 +120,13 @@ public class AuthlibInjectorDownloader {
return Optional.empty();
}
try {
return Optional.of(readArtifactInfo(artifactLocation));
return Optional.of(AuthlibInjectorArtifactInfo.from(artifactLocation));
} catch (IOException e) {
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
return Optional.empty();
}
}
private static AuthlibInjectorArtifactInfo readArtifactInfo(Path location) throws IOException {
try (JarFile jarFile = new JarFile(location.toFile())) {
Attributes attributes = jarFile.getManifest().getMainAttributes();
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
if (!"authlib-injector".equals(title)) {
throw new IOException("Bad Implementation-Title");
}
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
int buildNumber;
try {
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
.map(Integer::parseInt)
.orElseThrow(() -> new IOException("Missing Build-Number"));
} catch (NumberFormatException e) {
throw new IOException("Bad Build-Number", e);
}
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
}
}
private class AuthlibInjectorVersionInfo {
@SerializedName("build_number")
public int buildNumber;
@@ -20,25 +20,40 @@ package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Optional;
import java.util.logging.Level;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.JsonAdapter;
public class AuthlibInjectorServer {
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@JsonAdapter(AuthlibInjectorServer.Deserializer.class)
public class AuthlibInjectorServer implements Observable {
private static final int MAX_REDIRECTS = 5;
private static final Gson GSON = new GsonBuilder().create();
public static AuthlibInjectorServer locateServer(String url) throws IOException {
url = parseInputUrl(url);
@@ -60,8 +75,11 @@ public class AuthlibInjectorServer {
}
}
try {
return parseResponse(url, readFullyWithoutClosing(conn.getInputStream()));
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
server.refreshMetadata(readFullyWithoutClosing(conn.getInputStream()));
return server;
} finally {
conn.disconnect();
}
@@ -107,45 +125,83 @@ public class AuthlibInjectorServer {
}
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
try (InputStream in = new URL(url).openStream()) {
return parseResponse(url, readFullyWithoutClosing(in));
}
}
private static AuthlibInjectorServer parseResponse(String url, byte[] rawResponse) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(new String(rawResponse, UTF_8), JsonObject.class);
String name = extractServerName(response).orElse(url);
return new AuthlibInjectorServer(url, name);
} catch (JsonParseException e) {
throw new IOException("Malformed response", e);
}
}
private static Optional<String> extractServerName(JsonObject response){
return tryCast(response.get("meta"), JsonObject.class)
.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString));
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
server.refreshMetadata();
return server;
}
private String url;
private String name;
@Nullable
private String metadataResponse;
private long metadataTimestamp;
public AuthlibInjectorServer(String url, String name) {
@Nullable
private transient String name;
private transient boolean metadataRefreshed;
private transient ObservableHelper helper = new ObservableHelper(this);
public AuthlibInjectorServer(String url) {
this.url = url;
this.name = name;
}
public String getUrl() {
return url;
}
public String getName() {
return name;
public Optional<String> getMetadataResponse() {
return Optional.ofNullable(metadataResponse);
}
@Override
public String toString() {
return url + " (" + name + ")";
public long getMetadataTimestamp() {
return metadataTimestamp;
}
public String getName() {
return Optional.ofNullable(name)
.orElse(url);
}
public String fetchMetadataResponse() throws IOException {
if (metadataResponse == null || !metadataRefreshed) {
refreshMetadata();
}
return getMetadataResponse().get();
}
public void refreshMetadata() throws IOException {
refreshMetadata(readFullyAsByteArray(new URL(url).openStream()));
}
private void refreshMetadata(byte[] rawResponse) throws IOException {
long timestamp = System.currentTimeMillis();
String text = new String(rawResponse, UTF_8);
try {
setMetadataResponse(text, timestamp);
} catch (JsonParseException e) {
throw new IOException("Malformed response\n" + text, e);
}
metadataRefreshed = true;
LOG.info("authlib-injector server metadata refreshed: " + url);
Platform.runLater(helper::invalidate);
}
private void setMetadataResponse(String metadataResponse, long metadataTimestamp) throws JsonParseException {
JsonObject response = GSON.fromJson(metadataResponse, JsonObject.class);
if (response == null) {
throw new JsonParseException("Metadata response is empty");
}
synchronized (this) {
this.metadataResponse = metadataResponse;
this.metadataTimestamp = metadataTimestamp;
Optional<JsonObject> metaObject = tryCast(response.get("meta"), JsonObject.class);
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
.orElse(null);
}
}
@Override
@@ -162,4 +218,41 @@ public class AuthlibInjectorServer {
AuthlibInjectorServer another = (AuthlibInjectorServer) obj;
return this.url.equals(another.url);
}
@Override
public String toString() {
return name == null ? url : url + " (" + name + ")";
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public static class Deserializer implements JsonDeserializer<AuthlibInjectorServer> {
@Override
public AuthlibInjectorServer deserialize(JsonElement json, Type type, JsonDeserializationContext ctx) throws JsonParseException {
JsonObject jsonObj = json.getAsJsonObject();
AuthlibInjectorServer instance = new AuthlibInjectorServer(jsonObj.get("url").getAsString());
if (jsonObj.has("name")) {
instance.name = jsonObj.get("name").getAsString();
}
if (jsonObj.has("metadataResponse")) {
try {
instance.setMetadataResponse(jsonObj.get("metadataResponse").getAsString(), jsonObj.get("metadataTimestamp").getAsLong());
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Ignoring malformed metadata response cache: " + jsonObj.get("metadataResponse"), e);
}
}
return instance;
}
}
}
@@ -0,0 +1,49 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.logging.Level;
public class SimpleAuthlibInjectorArtifactProvider implements AuthlibInjectorArtifactProvider {
private Path location;
public SimpleAuthlibInjectorArtifactProvider(Path location) {
this.location = location;
}
@Override
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
return AuthlibInjectorArtifactInfo.from(location);
}
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
try {
return Optional.of(getArtifactInfo());
} catch (IOException e) {
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
return Optional.empty();
}
}
}
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type;
@@ -28,6 +30,7 @@ import java.util.Map;
*
* @author huangyuhui
*/
@JsonAdapter(Argument.Serializer.class)
@Immutable
public interface Argument extends Cloneable {
@@ -41,12 +44,6 @@ public interface Argument extends Cloneable {
List<String> toString(Map<String, String> keys, Map<String, Boolean> features);
class Serializer implements JsonDeserializer<Argument>, JsonSerializer<Argument> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public Argument deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonPrimitive())
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.ToStringBuilder;
@@ -35,6 +36,7 @@ import java.util.Optional;
*
* @author huangyuhui
*/
@JsonAdapter(Library.Serializer.class)
public class Library implements Comparable<Library> {
private final String groupId;
@@ -189,12 +191,6 @@ public class Library implements Comparable<Library> {
}
public static class Serializer implements JsonDeserializer<Library>, JsonSerializer<Library> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public Library deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE)
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.Immutable;
@@ -32,6 +33,7 @@ import java.util.stream.Collectors;
*
* @author huangyuhui
*/
@JsonAdapter(RuledArgument.Serializer.class)
@Immutable
public class RuledArgument implements Argument {
@@ -74,12 +76,6 @@ public class RuledArgument implements Argument {
}
public static class Serializer implements JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(RuledArgument src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -32,7 +33,6 @@ import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
@@ -117,8 +117,16 @@ public final class CurseCompletionTask extends Task {
try {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
} catch (FileNotFoundException e) {
notFound.set(true);
return file;
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (IOException | JsonParseException e2) {
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), e2);
notFound.set(true);
return file;
}
} catch (IOException ioe) {
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), ioe);
flag.set(false);
@@ -41,18 +41,22 @@ public final class CurseManifestFile implements Validation {
@SerializedName("fileName")
private final String fileName;
@SerializedName("url")
private final String url;
@SerializedName("required")
private final boolean required;
public CurseManifestFile() {
this(0, 0, "", true);
this(0, 0, null, null, true);
}
public CurseManifestFile(int projectID, int fileID, String fileName, boolean required) {
public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required) {
this.projectID = projectID;
this.fileID = fileID;
this.fileName = fileName;
this.url = url;
this.required = required;
}
@@ -79,11 +83,16 @@ public final class CurseManifestFile implements Validation {
}
public URL getUrl() {
return NetworkUtils.toURL("https://minecraft.curseforge.com/projects/" + projectID + "/files/" + fileID + "/download");
return url == null ? NetworkUtils.toURL("https://minecraft.curseforge.com/projects/" + projectID + "/files/" + fileID + "/download")
: NetworkUtils.toURL(url);
}
public CurseManifestFile withFileName(String fileName) {
return new CurseManifestFile(projectID, fileID, fileName, required);
return new CurseManifestFile(projectID, fileID, fileName, url, required);
}
public CurseManifestFile withURL(String url) {
return new CurseManifestFile(projectID, fileID, fileName, url, required);
}
@Override
@@ -0,0 +1,63 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.mod;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable;
@Immutable
public final class CurseMetaMod {
@SerializedName("Id")
private final int id;
@SerializedName("FileName")
private final String fileName;
@SerializedName("FileNameOnDisk")
private final String fileNameOnDisk;
@SerializedName("DownloadURL")
private final String downloadURL;
public CurseMetaMod() {
this(0, "", "", "");
}
public CurseMetaMod(int id, String fileName, String fileNameOnDisk, String downloadURL) {
this.id = id;
this.fileName = fileName;
this.fileNameOnDisk = fileNameOnDisk;
this.downloadURL = downloadURL;
}
public int getId() {
return id;
}
public String getFileName() {
return fileName;
}
public String getFileNameOnDisk() {
return fileNameOnDisk;
}
public String getDownloadURL() {
return downloadURL;
}
}
@@ -0,0 +1,36 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.task;
import java.io.IOException;
import java.net.URL;
public class DownloadException extends IOException {
private final URL url;
public DownloadException(URL url, Throwable cause) {
super("Unable to download " + url + ", " + cause.getMessage(), cause);
this.url = url;
}
public URL getUrl() {
return url;
}
}
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
@@ -328,7 +329,7 @@ public class FileDownloadTask extends Task {
}
if (exception != null)
throw new IOException("Unable to download file " + currentURL + ". " + exception.getMessage(), exception);
throw new DownloadException(currentURL, exception);
}
}
@@ -29,7 +29,6 @@ import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
@@ -141,7 +140,7 @@ public final class GetTask extends TaskResult<String> {
}
}
if (exception != null)
throw exception;
throw new DownloadException(url, exception);
}
/**
@@ -21,12 +21,6 @@ import java.io.File;
import java.util.Date;
import java.util.UUID;
import org.jackhuang.hmcl.game.Argument;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.RuledArgument;
import org.jackhuang.hmcl.game.StringArgument;
import org.jackhuang.hmcl.util.platform.Platform;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
@@ -39,13 +33,8 @@ public final class JsonUtils {
public static final Gson GSON = new GsonBuilder()
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.registerTypeAdapter(Library.class, Library.Serializer.INSTANCE)
.registerTypeAdapter(Argument.class, Argument.Serializer.INSTANCE)
.registerTypeAdapter(StringArgument.class, Argument.Serializer.INSTANCE)
.registerTypeAdapter(RuledArgument.class, RuledArgument.Serializer.INSTANCE)
.registerTypeAdapter(Date.class, DateTypeAdapter.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapter(Platform.class, Platform.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE)
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.util.io;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
@@ -88,7 +87,7 @@ public final class CompressingUtils {
*/
public static String readTextZipEntry(File zipFile, String name) throws IOException {
try (FileSystem fs = createReadOnlyZipFileSystem(zipFile.toPath())) {
return new String(Files.readAllBytes(fs.getPath(name)));
return FileUtils.readText(fs.getPath(name));
}
}
@@ -0,0 +1,167 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util.javafx;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.Property;
import javafx.beans.value.WritableValue;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
public final class PropertyUtils {
private PropertyUtils() {
}
public static class PropertyHandle {
public final WritableValue<Object> accessor;
public final Observable observable;
public PropertyHandle(WritableValue<Object> accessor, Observable observable) {
this.accessor = accessor;
this.observable = observable;
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Map<String, Function<Object, PropertyHandle>> getPropertyHandleFactories(Class<?> type) {
Map<String, Method> collectionGetMethods = new LinkedHashMap<>();
Map<String, Method> propertyMethods = new LinkedHashMap<>();
for (Method method : type.getMethods()) {
Class<?> returnType = method.getReturnType();
if (method.getParameterCount() == 0
&& !returnType.equals(void.class)) {
String name = method.getName();
if (name.endsWith("Property")) {
String propertyName = name.substring(0, name.length() - "Property".length());
if (!propertyName.isEmpty() && Property.class.isAssignableFrom(returnType)) {
propertyMethods.put(propertyName, method);
}
} else if (name.startsWith("get")) {
String propertyName = name.substring("get".length());
if (!propertyName.isEmpty() &&
(ObservableList.class.isAssignableFrom(returnType)
|| ObservableSet.class.isAssignableFrom(returnType)
|| ObservableMap.class.isAssignableFrom(returnType))) {
propertyName = Character.toLowerCase(propertyName.charAt(0)) + propertyName.substring(1);
collectionGetMethods.put(propertyName, method);
}
}
}
}
propertyMethods.keySet().forEach(collectionGetMethods::remove);
Map<String, Function<Object, PropertyHandle>> result = new LinkedHashMap<>();
propertyMethods.forEach((propertyName, method) -> {
result.put(propertyName, instance -> {
Property returnValue;
try {
returnValue = (Property<?>) method.invoke(instance);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
return new PropertyHandle(returnValue, returnValue);
});
});
collectionGetMethods.forEach((propertyName, method) -> {
result.put(propertyName, instance -> {
Object returnValue;
try {
returnValue = method.invoke(instance);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
WritableValue<Object> accessor;
if (returnValue instanceof ObservableList) {
accessor = new WritableValue<Object>() {
@Override
public Object getValue() {
return returnValue;
}
@Override
public void setValue(Object value) {
((ObservableList) returnValue).setAll((List) value);
}
};
} else if (returnValue instanceof ObservableSet) {
accessor = new WritableValue<Object>() {
@Override
public Object getValue() {
return returnValue;
}
@Override
public void setValue(Object value) {
ObservableSet target = (ObservableSet) returnValue;
target.clear();
target.addAll((Set) value);
}
};
} else if (returnValue instanceof ObservableMap) {
accessor = new WritableValue<Object>() {
@Override
public Object getValue() {
return returnValue;
}
@Override
public void setValue(Object value) {
ObservableMap target = (ObservableMap) returnValue;
target.clear();
target.putAll((Map) value);
}
};
} else {
throw new IllegalStateException();
}
return new PropertyHandle(accessor, (Observable) returnValue);
});
});
return result;
}
public static void copyProperties(Object from, Object to) {
Class<?> type = from.getClass();
while (!type.isInstance(to))
type = type.getSuperclass();
getPropertyHandleFactories(type)
.forEach((name, factory) -> {
PropertyHandle src = factory.apply(from);
PropertyHandle target = factory.apply(to);
target.accessor.setValue(src.accessor.getValue());
});
}
public static void attachListener(Object instance, InvalidationListener listener) {
getPropertyHandleFactories(instance.getClass())
.forEach((name, factory) -> {
factory.apply(instance).observable.addListener(listener);
});
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.platform;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
@@ -27,6 +28,7 @@ import java.lang.reflect.Type;
*
* @author huangyuhui
*/
@JsonAdapter(Platform.Serializer.class)
public enum Platform {
BIT_32("32"),
BIT_64("64"),
@@ -56,12 +58,6 @@ public enum Platform {
* The json serializer to {@link Platform}.
*/
public static class Serializer implements JsonSerializer<Platform>, JsonDeserializer<Platform> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(Platform t, Type type, JsonSerializationContext jsc) {
if (t == null)
+1
View File
@@ -118,3 +118,4 @@ No plugin API.
|`-Dhmcl.self_integrity_check.disable=true`|Bypass the self integrity check when checking for update.|
|`-Dhmcl.version.override=<version>`|Override the version number.|
|`-Dhmcl.update_source.override=<url>`|Override the update source.|
|`-Dhmcl.authlibinjector.location=<path>`|Use specified authlib-injector (instead of downloading one).|
+57 -55
View File
@@ -1,55 +1,57 @@
/*
* 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/}.
*/
group 'org.jackhuang'
version '3.0'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.tukaani:xz:1.8'
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'idea'
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
sourceCompatibility = 1.8
dependencies {
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
compile group: 'org.tukaani', name: 'xz', version: '1.8'
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
}
/*
* 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/}.
*/
group 'org.jackhuang'
version '3.0'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.tukaani:xz:1.8'
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'idea'
repositories {
mavenCentral()
jcenter()
maven { url 'https://jitpack.io' }
}
sourceCompatibility = 1.8
compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"
dependencies {
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
compile group: 'org.tukaani', name: 'xz', version: '1.8'
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
}
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip