Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx
This commit is contained in:
@@ -1,194 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
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.Accounts;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public final class AccountHelper {
|
||||
|
||||
private AccountHelper() {}
|
||||
|
||||
public static final File SKIN_DIR = Metadata.HMCL_DIRECTORY.resolve("skins").toFile();
|
||||
|
||||
public static void loadSkins() {
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
new SkinLoadTask((YggdrasilAccount) account, false).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Task loadSkinAsync(YggdrasilAccount account) {
|
||||
return new SkinLoadTask(account, false);
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account) {
|
||||
return new SkinLoadTask(account, true);
|
||||
}
|
||||
|
||||
private static File getSkinFile(UUID uuid) {
|
||||
return new File(SKIN_DIR, uuid + ".png");
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account) {
|
||||
return getSkin(account, 1);
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
|
||||
UUID uuid = account.getUUID();
|
||||
if (uuid == null)
|
||||
return getSteveSkin(scaleRatio);
|
||||
|
||||
File file = getSkinFile(uuid);
|
||||
if (file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
if (original.isError())
|
||||
return getDefaultSkin(uuid, scaleRatio);
|
||||
|
||||
return new Image("file:" + file.getAbsolutePath(),
|
||||
original.getWidth() * scaleRatio,
|
||||
original.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
return getDefaultSkin(uuid, scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio) throws Exception {
|
||||
File file = getSkinFile(profile.getId());
|
||||
downloadSkin(account, profile, true);
|
||||
if (!file.exists())
|
||||
return getDefaultSkin(profile.getId(), scaleRatio);
|
||||
|
||||
String url = "file:" + file.getAbsolutePath();
|
||||
return scale(url, scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getHead(Image skin, int scaleRatio) {
|
||||
final int size = 8 * scaleRatio;
|
||||
final int faceOffset = (int) Math.round(scaleRatio * 4d / 9d);
|
||||
BufferedImage image = SwingFXUtils.fromFXImage(skin, null);
|
||||
BufferedImage head = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = head.createGraphics();
|
||||
g2d.drawImage(image, faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
size, size, size + size, size + size, null);
|
||||
if (image.getHeight() > 32) {
|
||||
g2d.drawImage(image, 0, 0, size, size,
|
||||
40 * scaleRatio, 8 * scaleRatio, 48 * scaleRatio, 16 * scaleRatio, null);
|
||||
}
|
||||
return SwingFXUtils.toFXImage(head, null);
|
||||
}
|
||||
|
||||
private static class SkinLoadTask extends Task {
|
||||
private final YggdrasilAccount account;
|
||||
private final boolean refresh;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, boolean refresh) {
|
||||
this.account = account;
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
|
||||
DialogController.logIn(account);
|
||||
|
||||
downloadSkin(account, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
File file = getSkinFile(profile.getId());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
private static void downloadSkin(YggdrasilAccount account, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
if (account.getCharacter() == null) return;
|
||||
File file = getSkinFile(account.getUUID());
|
||||
if (!refresh && file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
if (!original.isError())
|
||||
return;
|
||||
}
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
public static Image scale(String url, double scaleRatio) {
|
||||
Image origin = new Image(url);
|
||||
return new Image(url,
|
||||
origin.getWidth() * scaleRatio,
|
||||
origin.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
|
||||
public static Image getSteveSkin(double scaleRatio) {
|
||||
return scale("/assets/img/steve.png", scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getAlexSkin(double scaleRatio) {
|
||||
return scale("/assets/img/alex.png", scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
|
||||
int type = uuid.hashCode() & 1;
|
||||
if (type == 1)
|
||||
return getAlexSkin(scaleRatio);
|
||||
else
|
||||
return getSteveSkin(scaleRatio);
|
||||
}
|
||||
}
|
||||
@@ -80,13 +80,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshVersions() {
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersionsAsync().start();
|
||||
@@ -168,6 +161,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return new Image("file:" + iconFile.getAbsolutePath());
|
||||
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
|
||||
return new Image("/assets/img/furnace.png");
|
||||
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
|
||||
return new Image("/assets/img/furnace.png");
|
||||
else
|
||||
return new Image("/assets/img/grass.png");
|
||||
}
|
||||
|
||||
209
HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java
Normal file
209
HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class TexturesLoader {
|
||||
|
||||
private TexturesLoader() {
|
||||
}
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
this.image = image;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
|
||||
|
||||
private static Path getTexturePath(Texture texture) {
|
||||
String url = texture.getUrl();
|
||||
int slash = url.lastIndexOf('/');
|
||||
int dot = url.lastIndexOf('.');
|
||||
if (dot < slash) {
|
||||
dot = url.length();
|
||||
}
|
||||
String hash = url.substring(slash + 1, dot);
|
||||
String prefix = hash.length() > 2 ? hash.substring(0, 2) : "xx";
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
Path file = getTexturePath(texture);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
// concurrency conflict?
|
||||
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
|
||||
} else {
|
||||
throw new IOException("Failed to download texture " + texture.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
}
|
||||
return new LoadedTexture(img, texture.getMetadata());
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
}
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
return DEFAULT_SKINS.get(model);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
|
||||
return MultiStepBinding.of(service.getProfileRepository().binding(uuid))
|
||||
.map(profile -> profile
|
||||
.flatMap(it -> {
|
||||
try {
|
||||
return YggdrasilService.getTextures(it);
|
||||
} catch (ServerResponseMalformedException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
})
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return getDefaultSkin(TextureModel.detectModelName(texture.getMetadata()));
|
||||
}
|
||||
} else {
|
||||
return uuidFallback;
|
||||
}
|
||||
}, uuidFallback, POOL);
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
g.drawImage(skin,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
|
||||
if (skin.getWidth() == skin.getHeight()) {
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return MultiStepBinding.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(img -> SwingFXUtils.toFXImage(img, null));
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -35,7 +35,6 @@ 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;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -63,7 +62,7 @@ public final class Accounts {
|
||||
private Accounts() {}
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -71,7 +70,7 @@ public final class Accounts {
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_YGGDRASIL);
|
||||
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||
@@ -94,7 +93,7 @@ public final class Accounts {
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_YGGDRASIL;
|
||||
return FACTORY_MOJANG;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
@@ -279,7 +278,7 @@ public final class Accounts {
|
||||
// ==== Login type name i18n ===
|
||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_YGGDRASIL, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
|
||||
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
|
||||
@@ -234,8 +234,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, String tooltip) {
|
||||
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 50, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
installFastTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 500, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
|
||||
|
||||
@@ -19,12 +19,13 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -36,6 +37,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
super(skinnable);
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
Pane placeholder = new Pane();
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
@@ -48,9 +50,12 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
list.setSpacing(10);
|
||||
list.setPadding(new Insets(10));
|
||||
|
||||
VBox content = new VBox();
|
||||
content.getChildren().setAll(list, placeholder);
|
||||
|
||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||
|
||||
scrollPane.setContent(list);
|
||||
scrollPane.setContent(content);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
@@ -86,7 +91,13 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
});
|
||||
}
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, vBox);
|
||||
// Keep a blank space to prevent buttons from blocking up mod items.
|
||||
BorderPane group = new BorderPane();
|
||||
group.setPickOnBounds(false);
|
||||
group.setBottom(vBox);
|
||||
placeholder.minHeightProperty().bind(vBox.heightProperty());
|
||||
|
||||
contentPane.getChildren().setAll(scrollPane, group);
|
||||
}
|
||||
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
@@ -87,11 +87,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
|
||||
TwoLineListItem prompt = new TwoLineListItem();
|
||||
prompt.setTitleFill(Color.WHITE);
|
||||
prompt.setSubtitleFill(Color.WHITE);
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.setStyle("-jfx-title-font-weight: BOLD;");
|
||||
prompt.titleProperty().bind(latestVersionProperty());
|
||||
|
||||
hBox.getChildren().setAll(lblIcon, prompt);
|
||||
|
||||
@@ -129,7 +129,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
// ==== Update ====
|
||||
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
updateListener = any -> {
|
||||
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ public abstract class SettingsView extends StackPane {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
cboFont = new FontComboBox(12);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
|
||||
@@ -38,23 +38,15 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
protected void invalidated() {
|
||||
Account account = get();
|
||||
if (account == null) {
|
||||
titleProperty().unbind();
|
||||
setTitle(i18n("account.missing"));
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageProperty().unbind();
|
||||
setImage(new Image("/assets/img/craft_table.png"));
|
||||
} else {
|
||||
setTitle(account.getCharacter());
|
||||
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
|
||||
setSubtitle(accountSubtitle(account));
|
||||
|
||||
final int scaleRatio = 4;
|
||||
Image defaultSkin = AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
|
||||
setImage(AccountHelper.getHead(defaultSkin, scaleRatio));
|
||||
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> {
|
||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
||||
setImage(AccountHelper.getHead(image, scaleRatio));
|
||||
});
|
||||
}
|
||||
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,21 +17,28 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
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.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class AccountListItem extends RadioButton {
|
||||
|
||||
private final Account account;
|
||||
@@ -44,23 +51,24 @@ public class AccountListItem extends RadioButton {
|
||||
getStyleClass().clear();
|
||||
setUserData(account);
|
||||
|
||||
StringBuilder subtitleString = new StringBuilder(Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)));
|
||||
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||
subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName());
|
||||
subtitle.bind(Bindings.concat(
|
||||
loginTypeName, ", ", i18n("account.injector.server"), ": ",
|
||||
Bindings.createStringBinding(server::getName, server)));
|
||||
} else {
|
||||
subtitle.set(loginTypeName);
|
||||
}
|
||||
|
||||
if (account instanceof OfflineAccount)
|
||||
title.set(account.getCharacter());
|
||||
else
|
||||
title.set(account.getUsername() + " - " + account.getCharacter());
|
||||
subtitle.set(subtitleString.toString());
|
||||
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
||||
if (account instanceof OfflineAccount) {
|
||||
title.bind(characterName);
|
||||
} else {
|
||||
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
final int scaleRatio = 4;
|
||||
Image image = account instanceof YggdrasilAccount ?
|
||||
AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) :
|
||||
AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
|
||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,19 +77,20 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
// progressBar.setVisible(true);
|
||||
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
|
||||
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
||||
// progressBar.setVisible(false);
|
||||
|
||||
if (isDependentsSucceeded) {
|
||||
final int scaleRatio = 4;
|
||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
account.clearCache();
|
||||
thread(() -> {
|
||||
try {
|
||||
account.logIn();
|
||||
} catch (CredentialExpiredException e) {
|
||||
try {
|
||||
DialogController.logIn(account);
|
||||
} catch (Exception e1) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
@@ -21,18 +21,26 @@ import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
|
||||
public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
public AccountListItemSkin(AccountListItem skinnable) {
|
||||
@@ -58,8 +66,22 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("title");
|
||||
title.textProperty().bind(skinnable.titleProperty());
|
||||
Label subtitle = new Label();
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
subtitle.textProperty().bind(skinnable.subtitleProperty());
|
||||
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
|
||||
Tooltip tooltip = new Tooltip();
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
|
||||
tooltip.textProperty().bind(Bindings.createStringBinding(server::toString, server));
|
||||
FXUtils.installSlowTooltip(subtitle, tooltip);
|
||||
}
|
||||
VBox item = new VBox(title, subtitle);
|
||||
item.getStyleClass().add("two-line-list-item");
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
center.getChildren().setAll(imageView, item);
|
||||
root.setCenter(center);
|
||||
|
||||
@@ -69,7 +91,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRefresh, i18n("button.refresh")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
@@ -77,14 +99,12 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRemove, i18n("button.delete")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ public class AccountLoginPane extends StackPane {
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
|
||||
@FXML
|
||||
private Label lblUsername;
|
||||
@FXML private Label lblUsername;
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private JFXProgressBar progressBar;
|
||||
|
||||
@@ -19,13 +19,17 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -35,20 +39,22 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
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.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
@@ -63,12 +69,13 @@ public class AddAccountPane extends StackPane {
|
||||
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
@FXML private Label lblInjectorServer;
|
||||
@FXML private Hyperlink linkManageInjectorServers;
|
||||
@FXML private JFXDialogLayout layout;
|
||||
@FXML private JFXButton btnAccept;
|
||||
@FXML private JFXButton btnAddServer;
|
||||
@FXML private JFXButton btnManageServer;
|
||||
@FXML private SpinnerPane acceptPane;
|
||||
@FXML private HBox linksContainer;
|
||||
|
||||
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
|
||||
|
||||
public AddAccountPane() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
|
||||
@@ -79,7 +86,7 @@ public class AddAccountPane extends StackPane {
|
||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||
selectDefaultServer();
|
||||
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
@@ -117,6 +124,34 @@ public class AddAccountPane extends StackPane {
|
||||
txtUsername.textProperty(),
|
||||
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(MultiStepBinding.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
.map(AddAccountPane::createHyperlinks)
|
||||
.map(FXCollections::observableList));
|
||||
Bindings.bindContent(linksContainer.getChildren(), links);
|
||||
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
}
|
||||
|
||||
private static final String[] ALLOWED_LINKS = { "register" };
|
||||
|
||||
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
if (server == null) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
Map<String, String> links = server.getLinks();
|
||||
List<Hyperlink> result = new ArrayList<>();
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
}
|
||||
}
|
||||
return unmodifiableList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +247,7 @@ public class AddAccountPane extends StackPane {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private GameProfile selectedProfile = null;
|
||||
|
||||
{
|
||||
public Selector() {
|
||||
setStyle("-fx-padding: 8px;");
|
||||
|
||||
cancel.setText(i18n("button.cancel"));
|
||||
@@ -230,45 +265,33 @@ public class AddAccountPane extends StackPane {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
if (!(account instanceof YggdrasilAccount))
|
||||
return CharacterSelector.DEFAULT.select(account, names);
|
||||
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
for (GameProfile profile : names) {
|
||||
Image image;
|
||||
final int scaleRatio = 4;
|
||||
try {
|
||||
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio);
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
|
||||
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.setImage(AccountHelper.getHead(image, scaleRatio));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
|
||||
JFXUtilities.runInFX(() -> Controllers.dialog(this));
|
||||
Controllers.dialog(this);
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
|
||||
if (selectedProfile == null)
|
||||
throw new NoSelectedCharacterException(account);
|
||||
throw new NoSelectedCharacterException();
|
||||
|
||||
return selectedProfile;
|
||||
} catch (InterruptedException ignore) {
|
||||
throw new NoSelectedCharacterException(account);
|
||||
throw new NoSelectedCharacterException();
|
||||
} finally {
|
||||
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
|
||||
}
|
||||
@@ -296,6 +319,8 @@ public class AddAccountPane extends StackPane {
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
@@ -33,17 +35,17 @@ public final class AuthlibInjectorServerItem extends BorderPane {
|
||||
private final AuthlibInjectorServer server;
|
||||
|
||||
private final Label lblServerName = new Label();
|
||||
private final Label lblServerIp = new Label();
|
||||
private final Label lblServerUrl = new Label();
|
||||
|
||||
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
|
||||
this.server = server;
|
||||
|
||||
lblServerName.setStyle("-fx-font-size: 15;");
|
||||
lblServerIp.setStyle("-fx-font-size: 10;");
|
||||
lblServerUrl.setStyle("-fx-font-size: 10;");
|
||||
|
||||
VBox center = new VBox();
|
||||
BorderPane.setAlignment(center, Pos.CENTER);
|
||||
center.getChildren().addAll(lblServerName, lblServerIp);
|
||||
center.getChildren().addAll(lblServerName, lblServerUrl);
|
||||
setCenter(center);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
@@ -55,8 +57,8 @@ public final class AuthlibInjectorServerItem extends BorderPane {
|
||||
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
lblServerName.setText(server.getName());
|
||||
lblServerIp.setText(server.getUrl());
|
||||
lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
|
||||
lblServerUrl.setText(server.getUrl());
|
||||
}
|
||||
|
||||
public AuthlibInjectorServer getServer() {
|
||||
|
||||
@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
|
||||
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
right.setOnMouseClicked(e -> onExplore());
|
||||
FXUtils.installTooltip(right, i18n("button.edit"));
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
setRight(right);
|
||||
|
||||
Tooltip tip = new Tooltip();
|
||||
|
||||
@@ -24,27 +24,27 @@ import static javafx.collections.FXCollections.singletonObservableList;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListCell;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
public class FontComboBox extends JFXComboBox<String> {
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
|
||||
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
|
||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
|
||||
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
||||
|
||||
setCellFactory(listView -> new ListCell<String>() {
|
||||
setCellFactory(listView -> new JFXListCell<String>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
public void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null) {
|
||||
if (!empty) {
|
||||
setText(item);
|
||||
setFont(new Font(item, fontSize));
|
||||
setGraphic(null);
|
||||
setStyle("-fx-font-family: \"" + item + "\"");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ public class IconedMenuItem extends IconedItem {
|
||||
}
|
||||
|
||||
public IconedMenuItem addTooltip(String tooltip) {
|
||||
FXUtils.installTooltip(this, tooltip);
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class ImagePickerItem extends BorderPane {
|
||||
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
|
||||
deleteButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
FXUtils.installTooltip(selectButton, i18n("button.edit"));
|
||||
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
|
||||
|
||||
HBox hBox = new HBox();
|
||||
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
|
||||
|
||||
@@ -17,15 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
@@ -45,16 +41,15 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private final JFXListView<Task> listBox = new JFXListView<>();
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
|
||||
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
||||
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
||||
|
||||
public TaskListPane() {
|
||||
getChildren().setAll(listBox);
|
||||
listBox.setSpacing(0);
|
||||
|
||||
listBox.setPadding(Insets.EMPTY);
|
||||
listBox.setCellFactory(listView -> new ProgressListNode());
|
||||
getChildren().setAll(listBox);
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
||||
@@ -70,7 +65,7 @@ public final class TaskListPane extends StackPane {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
listBox.getItems().clear();
|
||||
listBox.clear();
|
||||
finishedTasks.set(0);
|
||||
totTasks.set(0);
|
||||
});
|
||||
@@ -112,66 +107,64 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> listBox.getItems().add(task));
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
Platform.runLater(() -> listBox.add(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task task) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
Platform.runLater(() -> {
|
||||
if (listBox.getItems().remove(task))
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
listBox.remove(node);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Task task, Throwable throwable) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ProgressListNode extends ListCell<Task> {
|
||||
private final BorderPane borderPane = new BorderPane();
|
||||
private static class ProgressListNode extends BorderPane {
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
|
||||
{
|
||||
borderPane.setLeft(title);
|
||||
borderPane.setRight(state);
|
||||
borderPane.setBottom(bar);
|
||||
borderPane.setMinWidth(0);
|
||||
borderPane.setPrefWidth(1);
|
||||
public ProgressListNode(Task task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
title.setText(task.getName());
|
||||
state.textProperty().bind(task.messageProperty());
|
||||
|
||||
setPadding(Insets.EMPTY);
|
||||
setLeft(title);
|
||||
setRight(state);
|
||||
setBottom(bar);
|
||||
|
||||
bar.minWidthProperty().bind(widthProperty());
|
||||
bar.prefWidthProperty().bind(widthProperty());
|
||||
bar.maxWidthProperty().bind(widthProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Task item, boolean empty) {
|
||||
boolean wasEmpty = isEmpty();
|
||||
Task oldTask = getItem();
|
||||
public void unbind() {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
|
||||
if (!wasEmpty && oldTask != null) {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(borderPane);
|
||||
bar.visibleProperty().bind(Bindings.createBooleanBinding(() -> item.progressProperty().get() != -1, item.progressProperty()));
|
||||
bar.progressProperty().bind(item.progressProperty());
|
||||
state.textProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (item.getState() == Task.TaskState.FAILED) {
|
||||
return item.getLastException().getLocalizedMessage();
|
||||
} else {
|
||||
return item.messageProperty().get();
|
||||
}
|
||||
}, item.messageProperty(), item.stateProperty()));
|
||||
title.setText(item.getName());
|
||||
}
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,32 +19,15 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleablePropertyFactory;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TwoLineListItem extends StackPane {
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final StyleableObjectProperty<Font> titleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FONT, this, "title-font", Font.font(15));
|
||||
private final StyleableObjectProperty<Font> subtitleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FONT, this, "subtitle-font", Font.getDefault());
|
||||
|
||||
private final StyleableObjectProperty<Paint> titleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FILL, this, "title-fill", Color.BLACK);
|
||||
private final StyleableObjectProperty<Paint> subtitleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FILL, this, "subtitle-fill", Color.GRAY);
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
this();
|
||||
|
||||
@@ -55,19 +38,14 @@ public class TwoLineListItem extends StackPane {
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.textFillProperty().bind(titleFill);
|
||||
lblTitle.fontProperty().bind(titleFont);
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
lblSubtitle.textFillProperty().bind(subtitleFill);
|
||||
lblSubtitle.fontProperty().bind(subtitleFont);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.getChildren().setAll(lblTitle, lblSubtitle);
|
||||
getChildren().setAll(vbox);
|
||||
|
||||
getChildren().setAll(lblTitle, lblSubtitle);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@@ -95,74 +73,8 @@ public class TwoLineListItem extends StackPane {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public Font getTitleFont() {
|
||||
return titleFont.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Font> titleFontProperty() {
|
||||
return titleFont;
|
||||
}
|
||||
|
||||
public void setTitleFont(Font titleFont) {
|
||||
this.titleFont.set(titleFont);
|
||||
}
|
||||
|
||||
public Font getSubtitleFont() {
|
||||
return subtitleFont.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Font> subtitleFontProperty() {
|
||||
return subtitleFont;
|
||||
}
|
||||
|
||||
public void setSubtitleFont(Font subtitleFont) {
|
||||
this.subtitleFont.set(subtitleFont);
|
||||
}
|
||||
|
||||
public Paint getTitleFill() {
|
||||
return titleFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> titleFillProperty() {
|
||||
return titleFill;
|
||||
}
|
||||
|
||||
public void setTitleFill(Paint titleFill) {
|
||||
this.titleFill.set(titleFill);
|
||||
}
|
||||
|
||||
public Paint getSubtitleFill() {
|
||||
return subtitleFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> subtitleFillProperty() {
|
||||
return subtitleFill;
|
||||
}
|
||||
|
||||
public void setSubtitleFill(Paint subtitleFill) {
|
||||
this.subtitleFill.set(subtitleFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<TwoLineListItem> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
private static final CssMetaData<TwoLineListItem, Font> TITLE_FONT = FACTORY.createFontCssMetaData("-jfx-title-font", s -> s.titleFont, Font.font(15));
|
||||
private static final CssMetaData<TwoLineListItem, Font> SUBTITLE_FONT = FACTORY.createFontCssMetaData("-jfx-subtitle-font", s -> s.subtitleFont);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> TITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-title-fill", s -> s.titleFill);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> SUBTITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-subtitle-fill", s -> s.subtitleFill, Color.GREY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.mod.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||
@@ -111,6 +112,8 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
|
||||
}
|
||||
} else if (exception instanceof LibraryDownloadException) {
|
||||
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else if (exception instanceof DownloadException) {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -73,7 +74,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
||||
settings.put("failure_callback", new FailureCallback() {
|
||||
@Override
|
||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
||||
if (exception instanceof DownloadException) {
|
||||
if (exception instanceof LibraryDownloadException) {
|
||||
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_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);
|
||||
|
||||
@@ -44,7 +44,7 @@ public class DatapackListItem extends BorderPane {
|
||||
setCenter(modItem);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
FXUtils.installTooltip(btnRemove, i18n("datapack.remove"));
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
|
||||
@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnUpgrade, i18n("version.update")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class ModItem extends BorderPane {
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
JFXUtilities.runInFX(() -> {
|
||||
FXUtils.installTooltip(btnRemove, i18n("mods.remove"));
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
|
||||
});
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
|
||||
@@ -99,13 +99,13 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(() -> Versions.cleanVersion(profile, version), managementPopup)).addTooltip(i18n("version.manage.clean.tooltip"))
|
||||
);
|
||||
|
||||
FXUtils.installTooltip(btnDelete, i18n("version.manage.remove"));
|
||||
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
||||
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
|
||||
FXUtils.installFastTooltip(btnDelete, i18n("version.manage.remove"));
|
||||
FXUtils.installFastTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
||||
FXUtils.installFastTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installFastTooltip(btnExport, i18n("modpack.export"));
|
||||
|
||||
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
||||
FXUtils.installTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
FXUtils.installFastTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
|
||||
setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
@@ -60,6 +61,7 @@ import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage {
|
||||
@@ -85,7 +87,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
@FXML private ComponentList advancedSettingsPane;
|
||||
@FXML private ComponentList componentList;
|
||||
@FXML private ComponentList iconPickerItemWrapper;
|
||||
@FXML private JFXComboBox<?> cboLauncherVisibility;
|
||||
@FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
@FXML private JFXCheckBox chkFullscreen;
|
||||
@FXML private Label lblPhysicalMemory;
|
||||
@FXML private JFXToggleButton chkNoJVMArgs;
|
||||
@@ -100,6 +102,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
public VersionSettingsPage() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
|
||||
|
||||
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
|
||||
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase())));
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -81,8 +81,7 @@ public class Versions {
|
||||
|
||||
public static void updateGameAssets(Profile profile, String version) {
|
||||
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
|
||||
TaskExecutor executor = new GameAssetIndexDownloadTask(profile.getDependency(), resolvedVersion)
|
||||
.then(new GameAssetDownloadTask(profile.getDependency(), resolvedVersion))
|
||||
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
|
||||
.executor();
|
||||
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
|
||||
executor.start();
|
||||
|
||||
@@ -103,7 +103,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n";
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
|
||||
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
|
||||
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
|
||||
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
|
||||
|
||||
LOG.log(Level.SEVERE, text);
|
||||
|
||||
|
||||
@@ -104,20 +104,24 @@
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > .title {
|
||||
-fx-text-fill: black;
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .subtitle {
|
||||
-fx-text-fill: gray;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
-fx-background-color: gray;
|
||||
-fx-background-radius: 2px;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.bubble .two-line-list-item {
|
||||
-jfx-title-fill: white;
|
||||
-jfx-subtitle-fill: white;
|
||||
}
|
||||
|
||||
.two-line-list-item {
|
||||
-jfx-title-font-size: 15px;
|
||||
-jfx-title-fill: black;
|
||||
-jfx-subtitle-fill: gray;
|
||||
.bubble .two-line-list-item > .title,
|
||||
.bubble .two-line-list-item > .subtitle {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.window-title-bar .separator {
|
||||
@@ -338,7 +342,6 @@
|
||||
|
||||
.jfx-tool-bar HBox {
|
||||
-fx-alignment: center;
|
||||
/* -fx-spacing: 25.0;*/
|
||||
-fx-padding: 0.0 5.0;
|
||||
}
|
||||
|
||||
@@ -443,33 +446,6 @@
|
||||
-fx-fill: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button {
|
||||
-fx-font-size: 16.0px;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button .radio {
|
||||
-fx-stroke-width: 2.0px;
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-blue {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-jfx-selected-color: -fx-base-color;
|
||||
-jfx-unselected-color: #212121;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-red {
|
||||
-fx-text-fill: #f44336;
|
||||
-jfx-selected-color: #f44336;
|
||||
-jfx-unselected-color: #b71c1c;
|
||||
}
|
||||
|
||||
.custom-jfx-radio-button-green {
|
||||
-fx-text-fill: #4caf50;
|
||||
-jfx-selected-color: #4caf50;
|
||||
-jfx-unselected-color: #1b5e20;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Slider *
|
||||
@@ -510,18 +486,12 @@
|
||||
-fx-font-size: 10.0;
|
||||
}
|
||||
|
||||
/******************************************************/
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Rippler *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
/*.jfx-rippler {
|
||||
-fx-rippler-fill: -fx-base-color;
|
||||
-fx-mask-type: RECT;
|
||||
}*/
|
||||
.jfx-rippler:hover {
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
@@ -532,62 +502,24 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
.custom-jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: YELLOW;
|
||||
}
|
||||
|
||||
.custom-jfx-button-raised {
|
||||
-fx-padding: 0.7em 0.57em;
|
||||
-fx-font-size: 14.0px;
|
||||
-jfx-button-type: RAISED;
|
||||
-fx-background-color: rgb(102.0, 153.0, 102.0);
|
||||
-fx-pref-width: 200.0;
|
||||
-fx-text-fill: WHITE;
|
||||
}
|
||||
|
||||
.circle-jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: YELLOW;
|
||||
}
|
||||
|
||||
.circle-jfx-button-raised {
|
||||
-fx-padding: 0.7em 0.57em;
|
||||
-fx-font-size: 14.0px;
|
||||
-jfx-button-type: RAISED;
|
||||
-fx-background-color: rgb(102.0, 153.0, 102.0);
|
||||
-fx-pref-width: 200.0;
|
||||
-fx-text-fill: WHITE;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
.jfx-button-raised {
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: white;
|
||||
}
|
||||
|
||||
.jfx-button-raised .label {
|
||||
-fx-text-fill: white;
|
||||
.jfx-button-raised, .jfx-button-raised * {
|
||||
-fx-text-fill: -fx-base-text-fill;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
|
||||
.jfx-button-border {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-border-color: gray;
|
||||
-fx-border-radius: 5px;
|
||||
-fx-border-width: 0.2px;
|
||||
-fx-padding: 8px;
|
||||
}
|
||||
|
||||
.jfx-button-border .jfx-rippler {
|
||||
-jfx-rippler-fill: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.jfx-button-border .label {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
.jfx-button-border, .jfx-button-border * {
|
||||
-fx-text-fill: -fx-base-darker-color;
|
||||
}
|
||||
|
||||
.jfx-button-raised-round {
|
||||
@@ -606,20 +538,6 @@
|
||||
-jfx-checked-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box {
|
||||
-jfx-checked-color: RED;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box-all-colored {
|
||||
-jfx-checked-color: -fx-base-color;
|
||||
-jfx-unchecked-color: -fx-base-color;
|
||||
-fx-text-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.custom-jfx-check-box-text-colored {
|
||||
-fx-text-fill: rgb(153.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Progress Bar *
|
||||
@@ -643,15 +561,6 @@
|
||||
-fx-background-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-progress-bar > .bar {
|
||||
-fx-background-color: rgb(255.0, 128.0, 0.0);
|
||||
}
|
||||
|
||||
.custom-jfx-progress-bar-stroke > .bar {
|
||||
-fx-background-color: -fx-base-color;
|
||||
-fx-padding: 6;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Textfield *
|
||||
@@ -678,20 +587,12 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
.jfx-list-cell:odd,
|
||||
.jfx-list-cell:even,
|
||||
.list-cell:odd,
|
||||
.list-cell:even {
|
||||
.jfx-list-cell, .list-cell {
|
||||
-fx-background-color: WHITE;
|
||||
}
|
||||
|
||||
.list-cell:selected, .jfx-list-cell:selected {
|
||||
-fx-background-insets: 0.0;
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.jfx-list-cell:filled:hover,
|
||||
.jfx-list-cell:selected .label {
|
||||
.list-cell:selected, .jfx-list-cell:selected,
|
||||
.list-cell:hover, .jfx-list-cell:hover {
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
@@ -712,47 +613,6 @@
|
||||
-jfx-expanded: false;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
|
||||
.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
|
||||
-fx-background-color: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.custom-jfx-list-view {
|
||||
-fx-background-insets: 0.0;
|
||||
-jfx-cell-horizontal-margin: 0.0;
|
||||
-jfx-cell-vertical-margin: 5.0;
|
||||
-jfx-expanded: false;
|
||||
-fx-max-width: 200.0px;
|
||||
/* important to hide the list change of height */
|
||||
-fx-background-color: TRANSPARENT;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-rippler {
|
||||
-jfx-rippler-fill: RED;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view1 {
|
||||
-jfx-vertical-gap: 10.0;
|
||||
-fx-pref-width: 150px;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view-icon,
|
||||
.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
|
||||
-fx-fill: -fx-base-color;
|
||||
-fx-padding: 0.0 10.0 0.0 5.0;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view-icon-container {
|
||||
-fx-pref-width: 40px;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .sublist-item {
|
||||
-fx-border-color: #e0e0e0;
|
||||
-fx-border-width: 1 0 1 0;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
-fx-background-color: transparent;
|
||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
||||
@@ -802,20 +662,6 @@
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
|
||||
-fx-background-color: GRAY;
|
||||
}
|
||||
|
||||
.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
|
||||
-fx-background-color: BLACK;
|
||||
}
|
||||
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************/
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX SUBLIST IMPORTANT *
|
||||
@@ -855,10 +701,6 @@
|
||||
-fx-padding: 0 0 0 12;
|
||||
}
|
||||
|
||||
/*.custom-jfx-list-view .sublist-container {
|
||||
-fx-padding : 0 0 5 0;
|
||||
}*/
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Toggle Button *
|
||||
@@ -869,14 +711,6 @@
|
||||
-jfx-toggle-color: -fx-base-check-color;
|
||||
}
|
||||
|
||||
.custom-jfx-toggle-button {
|
||||
-jfx-toggle-color: #4285F4;
|
||||
}
|
||||
|
||||
.custom-jfx-toggle-button-red {
|
||||
-jfx-toggle-color: red;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
-fx-font-size: 14.0px;
|
||||
}
|
||||
@@ -1109,31 +943,10 @@
|
||||
-fx-fill: #D34336;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .label,
|
||||
.combo-box-popup .list-view .jfx-list-cell:filled:hover .label {
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .custom-jfx-list-view-icon,
|
||||
.combo-box-popup .list-view .jfx-list-cell:filled:hover .custom-jfx-list-view-icon,
|
||||
.combo-box-popup .list-view .jfx-list-cell:selected .custom-jfx-list-view-icon {
|
||||
-fx-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
|
||||
.combo-box-popup .list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
|
||||
-fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell {
|
||||
-fx-background-insets: 0.0;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell:odd,
|
||||
.combo-box-popup .list-view .jfx-list-cell:even {
|
||||
-fx-background-color: WHITE;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
|
||||
-jfx-rippler-fill: -fx-base-color;
|
||||
}
|
||||
@@ -1387,12 +1200,3 @@
|
||||
.fit-width {
|
||||
-fx-pref-width: 100%;
|
||||
}
|
||||
/*
|
||||
.jfx-scroll-pane .main-header {
|
||||
-fx-background-image: url("../bg1.jpg");
|
||||
}
|
||||
|
||||
.jfx-scroll-pane .condensed-header {
|
||||
-fx-background-image: url("../bg4.jpg");
|
||||
}
|
||||
*/
|
||||
@@ -32,6 +32,7 @@
|
||||
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
|
||||
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||
<HBox fx:id="linksContainer" alignment="CENTER_LEFT"/>
|
||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||
<graphic>
|
||||
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
|
||||
@@ -60,16 +60,7 @@
|
||||
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
|
||||
</left>
|
||||
<right>
|
||||
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<Label text="%settings.advanced.launcher_visibility.close"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.hide"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.keep"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.hide_reopen"/>
|
||||
</FXCollections>
|
||||
</items>
|
||||
</JFXComboBox>
|
||||
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ account.character=character
|
||||
account.choose=Choose a character
|
||||
account.create=Create a new account
|
||||
account.email=Email
|
||||
account.failed.character_deleted=The character has been deleted.
|
||||
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.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to another download source.
|
||||
@@ -49,6 +50,7 @@ account.injector.empty=Empty (Click the plus button right to add)
|
||||
account.injector.manage=Manage authentication servers
|
||||
account.injector.manage.title=Authentication servers
|
||||
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
|
||||
account.injector.link.register=Register
|
||||
account.injector.server=Auth Server
|
||||
account.injector.server_url=Server URL
|
||||
account.injector.server_name=Server Name
|
||||
@@ -307,7 +309,7 @@ settings.advanced.java_permanent_generation_space=PermGen Space/MB
|
||||
settings.advanced.jvm_args=Java VM Arguments
|
||||
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
|
||||
settings.advanced.launcher_visibility.hide=Hide the launcher when the game launched.
|
||||
settings.advanced.launcher_visibility.hide_reopen=Hide the launcher and re-open when game closes.
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=Hide the launcher and re-open when game closes.
|
||||
settings.advanced.launcher_visibility.keep=Keep the launcher visible.
|
||||
settings.advanced.launcher_visible=Launcher Visibility
|
||||
settings.advanced.minecraft_arguments=Minecraft Arguments
|
||||
|
||||
@@ -35,6 +35,7 @@ account.character=角色
|
||||
account.choose=選擇一個角色
|
||||
account.create=建立帳戶
|
||||
account.email=電子信箱
|
||||
account.failed.character_deleted=此角色已被刪除
|
||||
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
|
||||
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
|
||||
account.failed.injector_download_failure=無法下載 authlib-injector,請檢查網路或嘗試切換下載源
|
||||
@@ -48,6 +49,7 @@ account.injector.empty=無(點擊右側加號添加)
|
||||
account.injector.manage=管理認證伺服器
|
||||
account.injector.manage.title=認證伺服器
|
||||
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
||||
account.injector.link.register=註冊
|
||||
account.injector.server=認證伺服器
|
||||
account.injector.server_url=伺服器位址
|
||||
account.injector.server_name=伺服器名稱
|
||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(
|
||||
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
|
||||
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
|
||||
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
|
||||
settings.advanced.launcher_visibility.hide_reopen=隱藏啟動器並在遊戲結束後重新開啟
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
|
||||
settings.advanced.launcher_visibility.keep=不隱藏啟動器
|
||||
settings.advanced.launcher_visible=啟動器可見性
|
||||
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
|
||||
|
||||
@@ -35,6 +35,7 @@ account.character=角色
|
||||
account.choose=选择一个角色
|
||||
account.create=新建账户
|
||||
account.email=邮箱
|
||||
account.failed.character_deleted=此角色已被删除
|
||||
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
|
||||
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
||||
account.failed.injector_download_failure=无法下载 authlib-injector,请检查网络或尝试切换下载源
|
||||
@@ -48,6 +49,7 @@ account.injector.empty=无(点击右侧加号添加)
|
||||
account.injector.manage=管理认证服务器
|
||||
account.injector.manage.title=认证服务器
|
||||
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
||||
account.injector.link.register=注册
|
||||
account.injector.server=认证服务器
|
||||
account.injector.server_url=服务器地址
|
||||
account.injector.server_name=服务器名称
|
||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=内存永久保存区域(不
|
||||
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
|
||||
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
|
||||
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
|
||||
settings.advanced.launcher_visibility.hide_reopen=隐藏启动器并在游戏结束后重新打开
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=隐藏启动器并在游戏结束后重新打开
|
||||
settings.advanced.launcher_visibility.keep=保持启动器可见
|
||||
settings.advanced.launcher_visible=启动器可见性
|
||||
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
|
||||
|
||||
@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
|
||||
|
||||
public abstract Map<Object, Object> toStorage();
|
||||
|
||||
public abstract void clearCache();
|
||||
public void clearCache() {
|
||||
}
|
||||
|
||||
private ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
|
||||
@@ -17,27 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Select character by name.
|
||||
* Thrown when a previously existing character cannot be found.
|
||||
*/
|
||||
public class SpecificCharacterSelector implements CharacterSelector {
|
||||
private UUID uuid;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param uuid character's uuid.
|
||||
*/
|
||||
public SpecificCharacterSelector(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
public final class CharacterDeletedException extends AuthenticationException {
|
||||
public CharacterDeletedException() {
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,7 +34,6 @@ public interface CharacterSelector {
|
||||
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
|
||||
* @return your choice of game profile.
|
||||
*/
|
||||
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
GameProfile select(YggdrasilService yggdrasilService, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
|
||||
CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
}
|
||||
|
||||
@@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* (A account may hold more than one characters.)
|
||||
*/
|
||||
public final class NoCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
public NoCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoCharacterException() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NoSelectedCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param account the error yggdrasil account.
|
||||
*/
|
||||
public NoSelectedCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoSelectedCharacterException() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
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;
|
||||
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private AuthlibInjectorServer server;
|
||||
private final AuthlibInjectorServer server;
|
||||
private AuthlibInjectorArtifactProvider downloader;
|
||||
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
super(service, username, characterUUID, session);
|
||||
|
||||
this.downloader = downloader;
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
super(server.getYggdrasilService(), username, password, selector);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) {
|
||||
super(server.getYggdrasilService(), username, session);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password, selector));
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,7 +115,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||
"-Dauthlibinjector.side=client",
|
||||
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
super.clearCache();
|
||||
server.invalidateMetadataCache();
|
||||
}
|
||||
|
||||
public AuthlibInjectorServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
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.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
||||
|
||||
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
||||
|
||||
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, session.getSelectedProfile().getId(), session);
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
return new AuthlibInjectorAccount(server, downloader, username, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthlibInjectorVersionInfo {
|
||||
private static class AuthlibInjectorVersionInfo {
|
||||
@SerializedName("build_number")
|
||||
public int buildNumber;
|
||||
|
||||
|
||||
@@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return apiRoot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.emptyMap;
|
||||
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;
|
||||
@@ -29,9 +30,12 @@ import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -137,18 +141,25 @@ public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
@Nullable
|
||||
private transient String name;
|
||||
private transient Map<String, String> links = emptyMap();
|
||||
|
||||
private transient boolean metadataRefreshed;
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient YggdrasilService yggdrasilService;
|
||||
|
||||
public AuthlibInjectorServer(String url) {
|
||||
this.url = url;
|
||||
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return yggdrasilService;
|
||||
}
|
||||
|
||||
public Optional<String> getMetadataResponse() {
|
||||
return Optional.ofNullable(metadataResponse);
|
||||
}
|
||||
@@ -162,6 +173,10 @@ public class AuthlibInjectorServer implements Observable {
|
||||
.orElse(url);
|
||||
}
|
||||
|
||||
public Map<String, String> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public String fetchMetadataResponse() throws IOException {
|
||||
if (metadataResponse == null || !metadataRefreshed) {
|
||||
refreshMetadata();
|
||||
@@ -201,9 +216,23 @@ public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
|
||||
.orElse(null);
|
||||
this.links = metaObject.flatMap(meta -> tryCast(meta.get("links"), JsonObject.class))
|
||||
.map(linksObject -> {
|
||||
Map<String, String> converted = new LinkedHashMap<>();
|
||||
linksObject.entrySet().forEach(
|
||||
entry -> tryCast(entry.getValue(), JsonPrimitive.class).ifPresent(element -> {
|
||||
converted.put(entry.getKey(), element.getAsString());
|
||||
}));
|
||||
return converted;
|
||||
})
|
||||
.orElse(emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateMetadataCache() {
|
||||
metadataRefreshed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
|
||||
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
|
||||
OfflineAccount(String username, UUID uuid) {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(uuid);
|
||||
protected OfflineAccount(String username, UUID uuid) {
|
||||
this.username = requireNonNull(username);
|
||||
this.uuid = requireNonNull(uuid);
|
||||
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
|
||||
if (StringUtils.isBlank(username))
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(username))
|
||||
throw new AuthenticationException("Username cannot be empty");
|
||||
|
||||
public AuthInfo logIn() {
|
||||
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
return Optional.empty();
|
||||
return Optional.of(logIn());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
// Nothing to clear.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
@Immutable
|
||||
public class CompleteGameProfile extends GameProfile {
|
||||
|
||||
@JsonAdapter(PropertyMapSerializer.class)
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
|
||||
super(id, name);
|
||||
this.properties = requireNonNull(properties);
|
||||
}
|
||||
|
||||
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
|
||||
this(profile.getId(), profile.getName(), properties);
|
||||
}
|
||||
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
super.validate();
|
||||
|
||||
if (properties == null)
|
||||
throw new JsonParseException("Game profile properties cannot be null");
|
||||
}
|
||||
}
|
||||
@@ -17,36 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class GameProfile implements Validation {
|
||||
public class GameProfile implements Validation {
|
||||
|
||||
@JsonAdapter(UUIDTypeAdapter.class)
|
||||
private final UUID id;
|
||||
private final String name;
|
||||
private final PropertyMap properties;
|
||||
|
||||
public GameProfile() {
|
||||
this(null, null);
|
||||
}
|
||||
private final String name;
|
||||
|
||||
public GameProfile(UUID id, String name) {
|
||||
this(id, name, new PropertyMap());
|
||||
}
|
||||
|
||||
public GameProfile(UUID id, String name, PropertyMap properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.properties = properties;
|
||||
this.id = requireNonNull(id);
|
||||
this.name = requireNonNull(name);
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable
|
||||
*/
|
||||
public PropertyMap getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (id == null)
|
||||
throw new JsonParseException("Game profile id cannot be null or malformed");
|
||||
if (StringUtils.isBlank(name))
|
||||
throw new JsonParseException("Game profile name cannot be null or blank");
|
||||
throw new JsonParseException("Game profile id cannot be null");
|
||||
if (name == null)
|
||||
throw new JsonParseException("Game profile name cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
|
||||
|
||||
@Override
|
||||
public URL getAuthenticationURL() {
|
||||
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "mojang";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PropertyMap extends HashMap<String, String> {
|
||||
|
||||
public static PropertyMap fromMap(Map<?, ?> map) {
|
||||
PropertyMap propertyMap = new PropertyMap();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
|
||||
propertyMap.put((String) entry.getKey(), (String) entry.getValue());
|
||||
}
|
||||
return propertyMap;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
PropertyMap result = new PropertyMap();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Map.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", entry.getKey());
|
||||
object.addProperty("value", entry.getValue());
|
||||
result.add(object);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LegacySerializer
|
||||
implements JsonSerializer<PropertyMap> {
|
||||
public static final LegacySerializer INSTANCE = new LegacySerializer();
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject result = new JsonObject();
|
||||
for (PropertyMap.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonArray values = new JsonArray();
|
||||
values.add(new JsonPrimitive(entry.getValue()));
|
||||
result.add(entry.getKey(), values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
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.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
|
||||
|
||||
@Override
|
||||
public Map<String, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return unmodifiableMap(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
src.forEach((k, v) -> {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", k);
|
||||
object.addProperty("value", v);
|
||||
result.add(object);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,7 @@ public final class Texture {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getMetadata(String key) {
|
||||
if (metadata == null)
|
||||
return null;
|
||||
else
|
||||
return metadata.get(key);
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public enum TextureModel {
|
||||
STEVE("default"), ALEX("slim");
|
||||
|
||||
public final String modelName;
|
||||
|
||||
private TextureModel(String modelName) {
|
||||
this.modelName = modelName;
|
||||
}
|
||||
|
||||
public static TextureModel detectModelName(Map<String, String> metadata) {
|
||||
if (metadata != null && "slim".equals(metadata.get("model"))) {
|
||||
return ALEX;
|
||||
} else {
|
||||
return STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
public static TextureModel detectUUID(UUID uuid) {
|
||||
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,31 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
@Immutable
|
||||
public final class User implements Validation {
|
||||
|
||||
private final String id;
|
||||
private final PropertyMap properties;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public User(String id) {
|
||||
this(id, null);
|
||||
}
|
||||
|
||||
public User(String id, PropertyMap properties) {
|
||||
public User(String id, Map<String, String> properties) {
|
||||
this.id = id;
|
||||
this.properties = properties;
|
||||
}
|
||||
@@ -43,7 +51,7 @@ public final class User implements Validation {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -52,5 +60,4 @@ public final class User implements Validation {
|
||||
if (StringUtils.isBlank(id))
|
||||
throw new JsonParseException("User id cannot be empty.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,32 +17,62 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterDeletedException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class YggdrasilAccount extends Account {
|
||||
|
||||
private final String username;
|
||||
private final YggdrasilService service;
|
||||
private boolean isOnline = false;
|
||||
private final UUID characterUUID;
|
||||
private final String username;
|
||||
|
||||
private boolean authenticated = false;
|
||||
private YggdrasilSession session;
|
||||
private UUID characterUUID;
|
||||
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
this.service = service;
|
||||
this.username = username;
|
||||
this.session = session;
|
||||
this.characterUUID = characterUUID;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
|
||||
this.session = requireNonNull(session);
|
||||
}
|
||||
|
||||
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
|
||||
this.session = null;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new NoCharacterException();
|
||||
}
|
||||
|
||||
GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles());
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
characterToSelect);
|
||||
// response validity has been checked in refresh()
|
||||
} else {
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
characterUUID = session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
|
||||
return session.getSelectedProfile().getName();
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return session != null && StringUtils.isNotBlank(session.getAccessToken());
|
||||
}
|
||||
|
||||
public boolean canPlayOnline() {
|
||||
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||
if (!canPlayOnline()) {
|
||||
if (!authenticated) {
|
||||
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
isOnline = true;
|
||||
authenticated = true;
|
||||
} else {
|
||||
YggdrasilSession acquiredSession;
|
||||
try {
|
||||
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
|
||||
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
} catch (RemoteAuthenticationException e) {
|
||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||
throw new CredentialExpiredException(e);
|
||||
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (acquiredSession.getSelectedProfile() == null ||
|
||||
!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new ServerResponseMalformedException("Selected profile changed");
|
||||
}
|
||||
|
||||
session = acquiredSession;
|
||||
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
|
||||
}
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
|
||||
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
|
||||
* has a profile selected.
|
||||
*
|
||||
* @param acquiredSession the session acquired by making an authenticate/refresh request
|
||||
*/
|
||||
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
|
||||
throw new NoCharacterException(this);
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
|
||||
this.session = service.refresh(
|
||||
GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream()
|
||||
.filter(charatcer -> charatcer.getId().equals(characterUUID))
|
||||
.findFirst()
|
||||
.orElseThrow(CharacterDeletedException::new);
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
|
||||
characterToSelect);
|
||||
|
||||
} else {
|
||||
this.session = acquiredSession;
|
||||
if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
this.characterUUID = this.session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
|
||||
return Optional.of(session.toAuthInfo());
|
||||
|
||||
return Optional.empty();
|
||||
return Optional.of(session.toAuthInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toStorage() {
|
||||
if (session == null)
|
||||
throw new IllegalStateException("No session is specified");
|
||||
|
||||
HashMap<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", getUsername());
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", username);
|
||||
storage.putAll(session.toStorage());
|
||||
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
|
||||
storage.put("profileProperties", profile.getProperties());
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
if (session == null || session.getSelectedProfile() == null)
|
||||
return null;
|
||||
else
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin() throws AuthenticationException {
|
||||
return getSkin(session.getSelectedProfile());
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
|
||||
if (!service.getTextures(profile).isPresent()) {
|
||||
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
|
||||
}
|
||||
|
||||
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
Optional.ofNullable(session)
|
||||
.map(YggdrasilSession::getSelectedProfile)
|
||||
.map(GameProfile::getProperties)
|
||||
.ifPresent(it -> it.remove("textures"));
|
||||
authenticated = false;
|
||||
service.getProfileRepository().invalidate(characterUUID);
|
||||
}
|
||||
|
||||
private static String randomClientToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "YggdrasilAccount[username=" + getUsername() + "]";
|
||||
return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
@@ -34,10 +32,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
*/
|
||||
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
private YggdrasilService service;
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(password);
|
||||
|
||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new YggdrasilAccount(service, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
||||
|
||||
return new YggdrasilAccount(new YggdrasilService(provider), username, session.getSelectedProfile().getId(), session);
|
||||
}
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
public static String randomToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
return new YggdrasilAccount(service, username, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
|
||||
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public YggdrasilService(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
this.profileRepository = new ObservableOptionalCache<>(
|
||||
uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid + " from " + provider);
|
||||
return getCompleteGameProfile(uuid);
|
||||
},
|
||||
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid + " from " + provider, e),
|
||||
POOL);
|
||||
}
|
||||
|
||||
public ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> getProfileRepository() {
|
||||
return profileRepository;
|
||||
}
|
||||
|
||||
public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException {
|
||||
@@ -62,7 +85,7 @@ public class YggdrasilService {
|
||||
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
|
||||
}
|
||||
|
||||
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
private static Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("accessToken", accessToken);
|
||||
request.put("clientToken", clientToken);
|
||||
@@ -82,7 +105,16 @@ public class YggdrasilService {
|
||||
pair("name", characterToSelect.getName())));
|
||||
}
|
||||
|
||||
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
|
||||
if (characterToSelect != null) {
|
||||
if (response.getSelectedProfile() == null ||
|
||||
!response.getSelectedProfile().getId().equals(characterToSelect.getId())) {
|
||||
throw new ServerResponseMalformedException("Failed to select character");
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public boolean validate(String accessToken) throws AuthenticationException {
|
||||
@@ -121,20 +153,19 @@ public class YggdrasilService {
|
||||
* @param uuid the uuid that the character corresponding to.
|
||||
* @return the complete game profile(filled with more properties)
|
||||
*/
|
||||
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
Objects.requireNonNull(uuid);
|
||||
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), CompleteGameProfile.class));
|
||||
}
|
||||
|
||||
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
|
||||
public static Optional<Map<TextureType, Texture>> getTextures(CompleteGameProfile profile) throws ServerResponseMalformedException {
|
||||
Objects.requireNonNull(profile);
|
||||
|
||||
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
|
||||
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
|
||||
String encodedTextures = profile.getProperties().get("textures");
|
||||
|
||||
if (encodedTextures.isPresent()) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
|
||||
if (encodedTextures != null) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
|
||||
return Optional.ofNullable(texturePayload.textures);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
@@ -148,7 +179,12 @@ public class YggdrasilService {
|
||||
if (!clientToken.equals(response.clientToken))
|
||||
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
|
||||
|
||||
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
|
||||
return new YggdrasilSession(
|
||||
response.clientToken,
|
||||
response.accessToken,
|
||||
response.selectedProfile,
|
||||
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
|
||||
response.user);
|
||||
}
|
||||
|
||||
private static void requireEmpty(String response) throws AuthenticationException {
|
||||
@@ -168,7 +204,7 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private String request(URL url, Object payload) throws AuthenticationException {
|
||||
private static String request(URL url, Object payload) throws AuthenticationException {
|
||||
try {
|
||||
if (payload == null)
|
||||
return NetworkUtils.doGet(url);
|
||||
@@ -187,26 +223,25 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private class TextureResponse {
|
||||
private static class TextureResponse {
|
||||
public Map<TextureType, Texture> textures;
|
||||
}
|
||||
|
||||
private class AuthenticationResponse extends ErrorResponse {
|
||||
private static class AuthenticationResponse extends ErrorResponse {
|
||||
public String accessToken;
|
||||
public String clientToken;
|
||||
public GameProfile selectedProfile;
|
||||
public GameProfile[] availableProfiles;
|
||||
public List<GameProfile> availableProfiles;
|
||||
public User user;
|
||||
}
|
||||
|
||||
private class ErrorResponse {
|
||||
private static class ErrorResponse {
|
||||
public String error;
|
||||
public String errorMessage;
|
||||
public String cause;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
.create();
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -30,15 +31,16 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@Immutable
|
||||
public class YggdrasilSession {
|
||||
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
private GameProfile selectedProfile;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
private final String clientToken;
|
||||
private final String accessToken;
|
||||
private final GameProfile selectedProfile;
|
||||
private final List<GameProfile> availableProfiles;
|
||||
private final User user;
|
||||
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
|
||||
this.clientToken = clientToken;
|
||||
this.accessToken = accessToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
@@ -64,7 +66,7 @@ public class YggdrasilSession {
|
||||
/**
|
||||
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
||||
*/
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
public List<GameProfile> getAvailableProfiles() {
|
||||
return availableProfiles;
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ public class YggdrasilSession {
|
||||
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
|
||||
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
|
||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
|
||||
}
|
||||
|
||||
@@ -107,5 +109,5 @@ public class YggdrasilSession {
|
||||
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
|
||||
}
|
||||
|
||||
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
|
||||
private static final Gson GSON_PROPERTIES = new Gson();
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
return baseURL
|
||||
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
|
||||
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
|
||||
|
||||
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.download.game.LibrariesUniqueTask;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.download.game.*;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
@@ -74,7 +71,13 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
@Override
|
||||
public Task checkGameCompletionAsync(Version version) {
|
||||
return new ParallelTask(
|
||||
new GameAssetDownloadTask(this, version),
|
||||
Task.ofThen(var -> {
|
||||
if (!repository.getVersionJar(version).exists())
|
||||
return new GameDownloadTask(this, null, version);
|
||||
else
|
||||
return null;
|
||||
}),
|
||||
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY),
|
||||
new GameLibrariesTask(this, version)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
version = version.setId(name).setJar(null);
|
||||
variables.set("version", version);
|
||||
Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask(
|
||||
new GameAssetDownloadTask(dependencyManager, version),
|
||||
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
|
||||
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
|
||||
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant.
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ public class MaintainTask extends TaskResult<Version> {
|
||||
}
|
||||
|
||||
public static Version maintain(Version version) {
|
||||
if (version.getMainClass().contains("launchwrapper")) {
|
||||
return maintainGameWithLaunchWrapper(version);
|
||||
} else {
|
||||
// Vanilla Minecraft does not need maintain
|
||||
// Forge 1.13 support not implemented, not compatible with OptiFine currently.
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
private static Version maintainGameWithLaunchWrapper(Version version) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
||||
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.game.Artifact;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Immutable
|
||||
public class ForgeNewInstallProfile {
|
||||
|
||||
private final int spec;
|
||||
private final String minecraft;
|
||||
private final String json;
|
||||
private final List<Library> libraries;
|
||||
private final List<Processor> processors;
|
||||
private final Map<String, Datum> data;
|
||||
|
||||
public ForgeNewInstallProfile(int spec, String minecraft, String json, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
|
||||
this.spec = spec;
|
||||
this.minecraft = minecraft;
|
||||
this.json = json;
|
||||
this.libraries = libraries;
|
||||
this.processors = processors;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specification for install_profile.json.
|
||||
*/
|
||||
public int getSpec() {
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vanilla game version that this installer supports.
|
||||
*/
|
||||
public String getMinecraft() {
|
||||
return minecraft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version json to be installed.
|
||||
* @return path of the version json relative to the installer JAR file.
|
||||
*/
|
||||
public String getJson() {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Libraries that processors depend on.
|
||||
* @return the required dependencies.
|
||||
*/
|
||||
public List<Library> getLibraries() {
|
||||
return libraries == null ? Collections.emptyList() : libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tasks to be executed to setup modded environment.
|
||||
*/
|
||||
public List<Processor> getProcessors() {
|
||||
if (processors == null) return Collections.emptyList();
|
||||
return processors.stream().filter(p -> p.isSide("client")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for processors.
|
||||
*/
|
||||
public Map<String, String> getData() {
|
||||
if (data == null)
|
||||
return new HashMap<>();
|
||||
|
||||
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
|
||||
}
|
||||
|
||||
public static class Processor {
|
||||
private final List<String> sides;
|
||||
private final Artifact jar;
|
||||
private final List<Artifact> classpath;
|
||||
private final List<String> args;
|
||||
private final Map<String, String> outputs;
|
||||
|
||||
public Processor(List<String> sides, Artifact jar, List<Artifact> classpath, List<String> args, Map<String, String> outputs) {
|
||||
this.sides = sides;
|
||||
this.jar = jar;
|
||||
this.classpath = classpath;
|
||||
this.args = args;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which side this processor should be run on. We only support client install currently.
|
||||
* @param side can be one of "client", "server", "extract".
|
||||
* @return true if the processor can run on the side.
|
||||
*/
|
||||
public boolean isSide(String side) {
|
||||
return sides == null || sides.contains(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* The executable jar of this processor task. Will be executed in installation process.
|
||||
* @return the artifact path of executable jar.
|
||||
*/
|
||||
public Artifact getJar() {
|
||||
return jar;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dependencies of this processor task.
|
||||
* @return the artifact path of dependencies.
|
||||
*/
|
||||
public List<Artifact> getClasspath() {
|
||||
return classpath == null ? Collections.emptyList() : classpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments to pass to the processor jar.
|
||||
* Each item can be in one of the following formats:
|
||||
* [artifact]: An artifact path, used for locating files.
|
||||
* {entry}: Get corresponding value of the entry in {@link ForgeNewInstallProfile#getData()}
|
||||
* {MINECRAFT_JAR}: path of the Minecraft jar.
|
||||
* {SIDE}: values other than "client" will be ignored.
|
||||
* @return arguments to pass to the processor jar.
|
||||
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||
*/
|
||||
public List<String> getArgs() {
|
||||
return args == null ? Collections.emptyList() : args;
|
||||
}
|
||||
|
||||
/**
|
||||
* File-checksum pairs, used for verifying the output file is correct.
|
||||
* Arguments to pass to the processor jar.
|
||||
* Keys can be in one of [artifact] or {entry}. Should be file path.
|
||||
* Values can be in one of {entry} or 'literal'. Should be SHA-1 checksum.
|
||||
* @return files output from this processor.
|
||||
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
|
||||
*/
|
||||
public Map<String, String> getOutputs() {
|
||||
return outputs == null ? Collections.emptyMap() : outputs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Datum {
|
||||
private final String client;
|
||||
|
||||
public Datum(String client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be in the following formats:
|
||||
* [value]: An artifact path.
|
||||
* 'value': A string literal.
|
||||
* value: A file in the installer package, to be extracted to a temp folder, and then have the absolute path in replacements.
|
||||
* @return Value to use for the client install
|
||||
*/
|
||||
public String getClient() {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class ForgeNewInstallTask extends TaskResult<Version> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository gameRepository;
|
||||
private final Version version;
|
||||
private final Path installer;
|
||||
private final List<Task> dependents = new LinkedList<>();
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
private ForgeNewInstallProfile profile;
|
||||
private Version forgeVersion;
|
||||
|
||||
public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.gameRepository = dependencyManager.getGameRepository();
|
||||
this.version = version;
|
||||
this.installer = installer;
|
||||
}
|
||||
|
||||
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
|
||||
if (StringUtils.isSurrounded(literal, "{", "}"))
|
||||
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
|
||||
else if (StringUtils.isSurrounded(literal, "'", "'"))
|
||||
return StringUtils.removeSurrounding(literal, "'");
|
||||
else if (StringUtils.isSurrounded(literal, "[", "]"))
|
||||
return gameRepository.getArtifactFile(version, new Artifact(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
|
||||
else
|
||||
return plainConverter.apply(literal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "version";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
|
||||
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
|
||||
|
||||
for (Library library : profile.getLibraries()) {
|
||||
Path file = fs.getPath("maven").resolve(library.getPath());
|
||||
if (Files.exists(file)) {
|
||||
Path dest = gameRepository.getLibraryFile(version, library).toPath();
|
||||
FileUtils.copyFile(file, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path temp = Files.createTempDirectory("forge_installer");
|
||||
int finished = 0;
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
|
||||
Map<String, String> data = profile.getData();
|
||||
|
||||
updateProgress(0, processors.size());
|
||||
|
||||
for (Map.Entry<String, String> entry : data.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
data.put(key, parseLiteral(value,
|
||||
Collections.emptyMap(),
|
||||
str -> {
|
||||
Path dest = temp.resolve(str);
|
||||
FileUtils.copyFile(fs.getPath(str), dest);
|
||||
return dest.toString();
|
||||
}));
|
||||
}
|
||||
|
||||
data.put("SIDE", "client");
|
||||
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
|
||||
|
||||
for (ForgeNewInstallProfile.Processor processor : processors) {
|
||||
Map<String, String> outputs = new HashMap<>();
|
||||
boolean miss = false;
|
||||
|
||||
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
key = parseLiteral(key, data, ExceptionalFunction.identity());
|
||||
value = parseLiteral(value, data, ExceptionalFunction.identity());
|
||||
|
||||
if (key == null || value == null) {
|
||||
throw new Exception("Invalid forge installation configuration");
|
||||
}
|
||||
|
||||
outputs.put(key, value);
|
||||
|
||||
Path artifact = Paths.get(key);
|
||||
if (Files.exists(artifact)) {
|
||||
String code;
|
||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||
code = encodeHex(digest("SHA-1", stream));
|
||||
}
|
||||
|
||||
if (!Objects.equals(code, value)) {
|
||||
Files.delete(artifact);
|
||||
LOG.info("Found existing file is not valid: " + artifact);
|
||||
|
||||
miss = true;
|
||||
}
|
||||
} else {
|
||||
miss = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processor.getOutputs().isEmpty() && !miss) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
|
||||
if (!Files.isRegularFile(jar))
|
||||
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
|
||||
|
||||
String mainClass;
|
||||
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
||||
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(mainClass))
|
||||
throw new Exception("Game processor jar does not have main class " + jar);
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
|
||||
command.add("-cp");
|
||||
|
||||
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
|
||||
for (Artifact artifact : processor.getClasspath()) {
|
||||
Path file = gameRepository.getArtifactFile(version, artifact);
|
||||
if (!Files.isRegularFile(file))
|
||||
throw new Exception("Game processor dependency missing");
|
||||
classpath.add(file.toString());
|
||||
}
|
||||
classpath.add(jar.toString());
|
||||
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
||||
|
||||
command.add(mainClass);
|
||||
|
||||
List<String> args = processor.getArgs().stream().map(arg -> {
|
||||
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
|
||||
if (parsed == null)
|
||||
throw new IllegalStateException("Invalid forge installation configuration");
|
||||
return parsed;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
command.addAll(args);
|
||||
|
||||
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
|
||||
Process process = new ProcessBuilder(command).start();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
for (String line; (line = reader.readLine()) != null;) {
|
||||
System.out.println(line);
|
||||
}
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0)
|
||||
throw new IllegalStateException("Game processor exited abnormally");
|
||||
|
||||
for (Map.Entry<String, String> entry : outputs.entrySet()) {
|
||||
Path artifact = Paths.get(entry.getKey());
|
||||
if (!Files.isRegularFile(artifact))
|
||||
throw new FileNotFoundException("File missing: " + artifact);
|
||||
|
||||
String code;
|
||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||
code = encodeHex(digest("SHA-1", stream));
|
||||
}
|
||||
|
||||
if (!Objects.equals(code, entry.getValue())) {
|
||||
Files.delete(artifact);
|
||||
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(++finished, processors.size());
|
||||
}
|
||||
}
|
||||
|
||||
// resolve the version
|
||||
SimpleVersionProvider provider = new SimpleVersionProvider();
|
||||
provider.addVersion(version);
|
||||
|
||||
setResult(forgeVersion
|
||||
.setInheritsFrom(version.getId())
|
||||
.resolve(provider).setJar(null)
|
||||
.setId(version.getId()).setLogging(Collections.emptyMap()));
|
||||
|
||||
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
|
||||
|
||||
FileUtils.deleteDirectory(temp.toFile());
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,13 @@ public final class GameAssetDownloadTask extends Task {
|
||||
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.assetIndexInfo = version.getAssetIndex();
|
||||
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||
|
||||
if (!assetIndexFile.exists())
|
||||
if (!assetIndexFile.exists() || forceDownloadingIndex)
|
||||
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
|
||||
}
|
||||
|
||||
@@ -100,4 +100,6 @@ public final class GameAssetDownloadTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
public static final boolean DOWNLOAD_INDEX_FORCIBLY = true;
|
||||
public static final boolean DOWNLOAD_INDEX_IF_NECESSARY = false;
|
||||
}
|
||||
|
||||
@@ -56,13 +56,17 @@ public final class GameDownloadTask extends Task {
|
||||
public void execute() {
|
||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||
|
||||
dependencies.add(new FileDownloadTask(
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
|
||||
jar,
|
||||
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
|
||||
.setCaching(true)
|
||||
.setCacheRepository(dependencyManager.getCacheRepository())
|
||||
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar")));
|
||||
.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
|
||||
if (gameVersion != null)
|
||||
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar"));
|
||||
|
||||
dependencies.add(task);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
private final AbstractDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final List<Library> libraries;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
/**
|
||||
@@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||
this(dependencyManager, version, version.getLibraries());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the <b>resolved</b> version
|
||||
*/
|
||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.libraries = libraries;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||
if (!file.exists())
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||
|
||||
97
HMCLCore/src/main/java/org/jackhuang/hmcl/game/Artifact.java
Normal file
97
HMCLCore/src/main/java/org/jackhuang/hmcl/game/Artifact.java
Normal file
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Immutable
|
||||
@JsonAdapter(Artifact.Serializer.class)
|
||||
public final class Artifact {
|
||||
|
||||
private final String group;
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final String classifier;
|
||||
private final String extension;
|
||||
|
||||
private final String descriptor;
|
||||
private final String fileName;
|
||||
private final String path;
|
||||
|
||||
public Artifact(String descriptor) {
|
||||
this.descriptor = descriptor;
|
||||
|
||||
String[] arr = descriptor.split(":", 4);
|
||||
if (arr.length != 3 && arr.length != 4)
|
||||
throw new IllegalArgumentException("Artifact name is malformed");
|
||||
|
||||
String ext = null;
|
||||
int last = arr.length - 1;
|
||||
String[] splitted = arr[last].split("@");
|
||||
if (splitted.length == 2) {
|
||||
arr[last] = splitted[0];
|
||||
ext = splitted[1];
|
||||
} else if (splitted.length > 2) {
|
||||
throw new IllegalArgumentException("Artifact name is malformed");
|
||||
}
|
||||
|
||||
this.group = arr[0].replace("\\", "/");
|
||||
this.name = arr[1];
|
||||
this.version = arr[2];
|
||||
this.classifier = arr.length >= 4 ? arr[3] : null;
|
||||
this.extension = ext == null ? "jar" : ext;
|
||||
|
||||
String fileName = this.name + "-" + this.version;
|
||||
if (classifier != null) fileName += "-" + this.classifier;
|
||||
this.fileName = fileName + "." + this.extension;
|
||||
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return extension;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public Path getPath(Path root) {
|
||||
return root.resolve(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonDeserializer<Artifact>, JsonSerializer<Artifact> {
|
||||
@Override
|
||||
public JsonElement serialize(Artifact src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Artifact deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return json.isJsonPrimitive() ? new Artifact(json.getAsJsonPrimitive().getAsString()) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,18 +20,19 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.event.*;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* An implementation of classic Minecraft game repository.
|
||||
*
|
||||
@@ -81,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
||||
}
|
||||
|
||||
public Path getArtifactFile(Version version, Artifact artifact) {
|
||||
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
return getBaseDirectory();
|
||||
@@ -112,7 +117,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
public Version readVersionJson(File file) throws IOException, JsonParseException {
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), Version.class);
|
||||
return JsonUtils.fromNonNullJson(FileUtils.readText(file), Version.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,13 +176,13 @@ public class DefaultGameRepository implements GameRepository {
|
||||
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
|
||||
jsons.forEach(f -> {
|
||||
if (!f.delete())
|
||||
Logging.LOG.warning("Unable to delete file " + f);
|
||||
LOG.warning("Unable to delete file " + f);
|
||||
});
|
||||
// remove the version from version list regardless of whether the directory was removed successfully or not.
|
||||
try {
|
||||
FileUtils.deleteDirectory(removedFile);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
|
||||
LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -202,25 +207,31 @@ public class DefaultGameRepository implements GameRepository {
|
||||
// we will find the only json and rename it to correct name.
|
||||
if (!json.exists()) {
|
||||
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
|
||||
if (jsons.size() == 1)
|
||||
if (jsons.size() == 1) {
|
||||
LOG.info("Renaming json file " + jsons.get(0) + " to " + json);
|
||||
if (!jsons.get(0).renameTo(json)) {
|
||||
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
|
||||
LOG.warning("Cannot rename json file, ignoring version " + id);
|
||||
return Stream.empty();
|
||||
}
|
||||
} else {
|
||||
LOG.info("No available json file found, ignoring version " + id);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
Version version;
|
||||
try {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
version = readVersionJson(json);
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Malformed version json " + id, e);
|
||||
// JsonSyntaxException or IOException or NullPointerException(!!)
|
||||
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
|
||||
return Stream.empty();
|
||||
|
||||
try {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
version = readVersionJson(json);
|
||||
} catch (Exception e2) {
|
||||
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
|
||||
LOG.log(Level.SEVERE, "User corrected version json is still malformed", e2);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
@@ -230,7 +241,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
try {
|
||||
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
|
||||
LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.", e);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
@@ -246,7 +257,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY)
|
||||
versions.put(version.getId(), version);
|
||||
} catch (VersionNotFoundException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version.");
|
||||
LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,10 +269,8 @@ public class DefaultGameRepository implements GameRepository {
|
||||
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
||||
return;
|
||||
|
||||
Schedulers.newThread().schedule(() -> {
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
});
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -278,7 +287,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
try {
|
||||
return reconstructAssets(version, assetId);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e);
|
||||
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e);
|
||||
return getAssetDirectory(version, assetId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
/**
|
||||
@@ -74,8 +75,8 @@ public class DownloadInfo implements Validation {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (StringUtils.isBlank(url))
|
||||
throw new JsonParseException("DownloadInfo url can not be null");
|
||||
throw new TolerableValidationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||
@@ -30,6 +36,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@@ -37,6 +44,16 @@ import java.util.stream.StreamSupport;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameVersion {
|
||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
||||
try {
|
||||
MinecraftVersion version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), MinecraftVersion.class);
|
||||
return Optional.ofNullable(version.name);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
@@ -74,6 +91,13 @@ public final class GameVersion {
|
||||
return Optional.empty();
|
||||
|
||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
||||
Path versionJson = gameJar.getPath("version.json");
|
||||
if (Files.exists(versionJson)) {
|
||||
Optional<String> result = getVersionFromJson(versionJson);
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
@@ -88,4 +112,24 @@ public final class GameVersion {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MinecraftVersion {
|
||||
public String name;
|
||||
|
||||
@SerializedName("release_target")
|
||||
public String releaseTarget;
|
||||
|
||||
public String id;
|
||||
|
||||
public boolean stable;
|
||||
|
||||
@SerializedName("world_version")
|
||||
public int worldVersion;
|
||||
|
||||
@SerializedName("protocol_version")
|
||||
public int protocolVersion;
|
||||
|
||||
@SerializedName("pack_version")
|
||||
public int packVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -54,7 +55,7 @@ public class IdDownloadInfo extends DownloadInfo {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
super.validate();
|
||||
|
||||
if (StringUtils.isBlank(id))
|
||||
|
||||
@@ -45,13 +45,13 @@ public class Library implements Comparable<Library> {
|
||||
private final String classifier;
|
||||
private final String url;
|
||||
private final LibrariesDownloadInfo downloads;
|
||||
private final LibraryDownloadInfo download;
|
||||
private transient final LibraryDownloadInfo download;
|
||||
private final ExtractRules extract;
|
||||
private final Map<OperatingSystem, String> natives;
|
||||
private final List<CompatibilityRule> rules;
|
||||
private final List<String> checksums;
|
||||
|
||||
private final String path;
|
||||
private transient final String path;
|
||||
|
||||
public Library(String groupId, String artifactId, String version) {
|
||||
this(groupId, artifactId, version, null, null, null);
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
/**
|
||||
@@ -66,7 +67,7 @@ public final class LoggingInfo implements Validation {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
file.validate();
|
||||
if (StringUtils.isBlank(argument))
|
||||
throw new JsonParseException("LoggingInfo.argument is empty.");
|
||||
|
||||
@@ -40,7 +40,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -133,7 +132,7 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Version version = Objects.requireNonNull(repository.readVersionJson(name));
|
||||
Version version = repository.readVersionJson(name);
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
@@ -55,18 +58,14 @@ public abstract class Task {
|
||||
this.significance = significance;
|
||||
}
|
||||
|
||||
private ReadOnlyObjectWrapper<TaskState> state = new ReadOnlyObjectWrapper<>(this, "state", TaskState.READY);
|
||||
private TaskState state = TaskState.READY;
|
||||
|
||||
public TaskState getState() {
|
||||
return state.get();
|
||||
return state;
|
||||
}
|
||||
|
||||
void setState(TaskState state) {
|
||||
this.state.setValue(state);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<TaskState> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
private Throwable lastException = null;
|
||||
@@ -340,10 +339,6 @@ public abstract class Task {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task empty() {
|
||||
return of(ExceptionalConsumer.empty());
|
||||
}
|
||||
|
||||
public static Task of(String name, ExceptionalRunnable<?> runnable) {
|
||||
return of(name, ExceptionalConsumer.fromRunnable(runnable));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
@@ -31,9 +32,11 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public class InvocationDispatcher<ARG> implements Consumer<ARG> {
|
||||
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.accept(() -> {
|
||||
action.accept(arg.get());
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.execute(() -> {
|
||||
synchronized (action) {
|
||||
action.accept(arg.get());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -172,6 +176,17 @@ public final class Lang {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor threadPool(String name, boolean daemon, int threads, long timeout, TimeUnit timeunit) {
|
||||
AtomicInteger counter = new AtomicInteger(1);
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(0, threads, timeout, timeunit, new LinkedBlockingQueue<>(), r -> {
|
||||
Thread t = new Thread(r, name + "-" + counter.getAndIncrement());
|
||||
t.setDaemon(daemon);
|
||||
return t;
|
||||
});
|
||||
pool.allowsCoreThreadTimeOut();
|
||||
return pool;
|
||||
}
|
||||
|
||||
public static int parseInt(Object string, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(string.toString());
|
||||
@@ -189,14 +204,6 @@ public final class Lang {
|
||||
}
|
||||
}
|
||||
|
||||
public static Double toDoubleOrNull(Object string) {
|
||||
try {
|
||||
return Double.parseDouble(string.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first non-null reference in given list.
|
||||
* @param t nullable references list.
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Collection;
|
||||
@@ -24,8 +26,6 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -128,6 +128,10 @@ public final class StringUtils {
|
||||
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
|
||||
}
|
||||
|
||||
public static boolean isSurrounded(String str, String prefix, String suffix) {
|
||||
return str.startsWith(prefix) && str.endsWith(suffix);
|
||||
}
|
||||
|
||||
public static String removeSurrounding(String str, String delimiter) {
|
||||
return removeSurrounding(str, delimiter, delimiter);
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
|
||||
*/
|
||||
public interface ExceptionalFunction<T, R, E extends Exception> {
|
||||
R apply(T t) throws E;
|
||||
|
||||
static <T, E extends RuntimeException> ExceptionalFunction<T, T, E> identity() {
|
||||
return t -> t;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,24 +15,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
package org.jackhuang.hmcl.util.gson;
|
||||
|
||||
/**
|
||||
* What's circle dependency?
|
||||
* When C inherits from B, and B inherits from something else, and finally inherits from C again.
|
||||
*
|
||||
* @author huangyuhui
|
||||
* This exception gets thrown by implementations of {@link Validation#validate()} if you want to replace
|
||||
* the nullable JSON-parsed object which does not satisfy the constraint with null value.
|
||||
* @see Validation
|
||||
*/
|
||||
public final class CircleDependencyException extends GameException {
|
||||
public final class TolerableValidationException extends Exception {
|
||||
|
||||
public CircleDependencyException() {
|
||||
}
|
||||
|
||||
public CircleDependencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CircleDependencyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
public TolerableValidationException() {
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
||||
|
||||
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
|
||||
|
||||
private UUIDTypeAdapter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter writer, UUID value) throws IOException {
|
||||
writer.value(value == null ? null : fromUUID(value));
|
||||
|
||||
@@ -36,5 +36,5 @@ public interface Validation {
|
||||
*
|
||||
* @throws JsonParseException if fields are filled in wrong format or wrong type.
|
||||
*/
|
||||
void validate() throws JsonParseException;
|
||||
void validate() throws JsonParseException, TolerableValidationException;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,14 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
|
||||
return new TypeAdapter<T>() {
|
||||
@Override
|
||||
public void write(JsonWriter writer, T t) throws IOException {
|
||||
if (t instanceof Validation)
|
||||
((Validation) t).validate();
|
||||
if (t instanceof Validation) {
|
||||
try {
|
||||
((Validation) t).validate();
|
||||
} catch (TolerableValidationException e) {
|
||||
delegate.write(writer, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
delegate.write(writer, t);
|
||||
}
|
||||
@@ -49,8 +55,13 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Override
|
||||
public T read(JsonReader reader) throws IOException {
|
||||
T t = delegate.read(reader);
|
||||
if (t instanceof Validation)
|
||||
((Validation) t).validate();
|
||||
if (t instanceof Validation) {
|
||||
try {
|
||||
((Validation) t).validate();
|
||||
} catch (TolerableValidationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,6 +275,22 @@ public final class FileUtils {
|
||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void copyFile(Path srcFile, Path destFile)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||
Objects.requireNonNull(destFile, "Destination must not be null");
|
||||
if (!Files.exists(srcFile))
|
||||
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
|
||||
if (Files.isDirectory(srcFile))
|
||||
throw new IOException("Source '" + srcFile + "' exists but is a directory");
|
||||
Path parentFile = destFile.getParent();
|
||||
Files.createDirectories(parentFile);
|
||||
if (Files.exists(destFile) && !Files.isWritable(destFile))
|
||||
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||
|
||||
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||
copyFile(srcFile, destFile);
|
||||
srcFile.delete();
|
||||
|
||||
@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
return new FlatMappedBinding<>(map(mapper), nullAlternative);
|
||||
}
|
||||
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<U, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(this, mapper, executor, initial);
|
||||
}
|
||||
|
||||
private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
|
||||
|
||||
public SimpleBinding(ObservableValue<T> predecessor) {
|
||||
@@ -68,6 +77,11 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
|
||||
return new MappedBinding<>(predecessor, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<T, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(predecessor, mapper, executor, initial);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
@@ -119,4 +133,52 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AsyncMappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
|
||||
private final InvocationDispatcher<T> dispatcher;
|
||||
|
||||
private boolean initialized = false;
|
||||
private T prev;
|
||||
private U value;
|
||||
|
||||
public AsyncMappedBinding(ObservableValue<T> predecessor, Function<T, U> mapper, Executor executor, U initial) {
|
||||
super(predecessor);
|
||||
this.value = initial;
|
||||
|
||||
dispatcher = InvocationDispatcher.runOn(executor, arg -> {
|
||||
synchronized (this) {
|
||||
if (initialized && Objects.equals(arg, prev)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
U newValue = mapper.apply(arg);
|
||||
synchronized (this) {
|
||||
prev = arg;
|
||||
value = newValue;
|
||||
initialized = true;
|
||||
}
|
||||
Platform.runLater(this::invalidate);
|
||||
});
|
||||
}
|
||||
|
||||
// called on FX thread, this method is serial
|
||||
@Override
|
||||
protected U computeValue() {
|
||||
T currentPrev = predecessor.getValue();
|
||||
U value;
|
||||
boolean updateNeeded = false;
|
||||
synchronized (this) {
|
||||
value = this.value;
|
||||
if (!initialized || !Objects.equals(currentPrev, prev)) {
|
||||
updateNeeded = true;
|
||||
}
|
||||
}
|
||||
if (updateNeeded) {
|
||||
dispatcher.accept(currentPrev);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableCache<K, V, E extends Exception> {
|
||||
|
||||
private final ExceptionalFunction<K, V, E> source;
|
||||
private final BiConsumer<K, Throwable> exceptionHandler;
|
||||
private final V fallbackValue;
|
||||
private final Executor executor;
|
||||
private final ObservableHelper observable = new ObservableHelper();
|
||||
private final Map<K, V> cache = new HashMap<>();
|
||||
private final Map<K, CompletableFuture<V>> pendings = new HashMap<>();
|
||||
private final Map<K, Boolean> invalidated = new HashMap<>();
|
||||
|
||||
public ObservableCache(ExceptionalFunction<K, V, E> source, BiConsumer<K, Throwable> exceptionHandler, V fallbackValue, Executor executor) {
|
||||
this.source = source;
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
this.fallbackValue = fallbackValue;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
synchronized (this) {
|
||||
return Optional.ofNullable(cache.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
synchronized (this) {
|
||||
cache.put(key, value);
|
||||
invalidated.remove(key);
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
|
||||
private CompletableFuture<V> query(K key, Executor executor) {
|
||||
CompletableFuture<V> future;
|
||||
synchronized (this) {
|
||||
CompletableFuture<V> prev = pendings.get(key);
|
||||
if (prev != null) {
|
||||
return prev;
|
||||
} else {
|
||||
future = new CompletableFuture<>();
|
||||
pendings.put(key, future);
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
V result;
|
||||
try {
|
||||
result = source.apply(key);
|
||||
} catch (Throwable ex) {
|
||||
synchronized (this) {
|
||||
pendings.remove(key);
|
||||
}
|
||||
exceptionHandler.accept(key, ex);
|
||||
future.completeExceptionally(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
cache.put(key, result);
|
||||
invalidated.remove(key);
|
||||
pendings.remove(key, future);
|
||||
}
|
||||
future.complete(result);
|
||||
Platform.runLater(observable::invalidate);
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V cached;
|
||||
synchronized (this) {
|
||||
cached = cache.get(key);
|
||||
if (cached != null && !invalidated.containsKey(key)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return query(key, Runnable::run).join();
|
||||
} catch (CompletionException | CancellationException ignored) {
|
||||
}
|
||||
|
||||
if (cached == null) {
|
||||
return fallbackValue;
|
||||
} else {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
public V getDirectly(K key) throws E {
|
||||
V result = source.apply(key);
|
||||
put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public ObjectBinding<V> binding(K key) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
V result;
|
||||
boolean refresh;
|
||||
synchronized (this) {
|
||||
result = cache.get(key);
|
||||
if (result == null) {
|
||||
result = fallbackValue;
|
||||
refresh = true;
|
||||
} else {
|
||||
refresh = invalidated.containsKey(key);
|
||||
}
|
||||
}
|
||||
if (refresh) {
|
||||
query(key, executor);
|
||||
}
|
||||
return result;
|
||||
}, observable);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
synchronized (this) {
|
||||
if (cache.containsKey(key)) {
|
||||
invalidated.put(key, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener {
|
||||
private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private Observable source;
|
||||
|
||||
public ObservableHelper() {
|
||||
this.source = this;
|
||||
}
|
||||
|
||||
public ObservableHelper(Observable source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableOptionalCache<K, V, E extends Exception> {
|
||||
|
||||
private final ObservableCache<K, Optional<V>, E> backed;
|
||||
|
||||
public ObservableOptionalCache(ExceptionalFunction<K, Optional<V>, E> source, BiConsumer<K, Throwable> exceptionHandler, Executor executor) {
|
||||
backed = new ObservableCache<>(source, exceptionHandler, Optional.empty(), executor);
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
return backed.getImmediately(key).flatMap(it -> it);
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
backed.put(key, Optional.of(value));
|
||||
}
|
||||
|
||||
public Optional<V> get(K key) {
|
||||
return backed.get(key);
|
||||
}
|
||||
|
||||
public Optional<V> getDirectly(K key) throws E {
|
||||
return backed.getDirectly(key);
|
||||
}
|
||||
|
||||
public ObjectBinding<Optional<V>> binding(K key) {
|
||||
return backed.binding(key);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
backed.invalidate(key);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.util.*;
|
||||
/**
|
||||
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
|
||||
* Apache License 2.0
|
||||
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
|
||||
*/
|
||||
public class VersionNumber implements Comparable<VersionNumber> {
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@
|
||||
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>HMCL</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformVersion>7.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
@@ -152,19 +152,25 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="HMCL.h" />
|
||||
<ClInclude Include="java.h" />
|
||||
<ClInclude Include="main.h" />
|
||||
<ClInclude Include="os.h" />
|
||||
<ClInclude Include="Resource.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="Version.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="HMCL.cpp" />
|
||||
<ClCompile Include="java.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="os.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>
|
||||
<ClCompile Include="Version.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="HMCL.rc" />
|
||||
|
||||
@@ -24,7 +24,16 @@
|
||||
<ClInclude Include="Resource.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="HMCL.h">
|
||||
<ClInclude Include="main.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Version.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="java.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="os.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
@@ -32,7 +41,16 @@
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="HMCL.cpp">
|
||||
<ClCompile Include="main.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Version.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="java.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="os.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
|
||||
15
HMCLauncher/HMCL/Version.cpp
Normal file
15
HMCLauncher/HMCL/Version.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "stdafx.h"
|
||||
#include "Version.h"
|
||||
|
||||
Version::Version(const std::wstring & rawString)
|
||||
{
|
||||
int idx = 0;
|
||||
ver[0] = ver[1] = ver[2] = ver[3] = 0;
|
||||
for (auto &i : rawString)
|
||||
{
|
||||
if (idx >= 4) break;
|
||||
if (i == '.') ++idx;
|
||||
else if (i == '_') ++idx;
|
||||
else if (isdigit(i)) ver[idx] = ver[idx] * 10 + (i - L'0');
|
||||
}
|
||||
}
|
||||
20
HMCLauncher/HMCL/Version.h
Normal file
20
HMCLauncher/HMCL/Version.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class Version
|
||||
{
|
||||
public:
|
||||
int ver[4];
|
||||
|
||||
Version(const std::wstring &rawString);
|
||||
|
||||
bool operator<(const Version &other) const
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (ver[i] != other.ver[i])
|
||||
return ver[i] < other.ver[i];
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
88
HMCLauncher/HMCL/java.cpp
Normal file
88
HMCLauncher/HMCL/java.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "stdafx.h"
|
||||
#include "java.h"
|
||||
#include "os.h"
|
||||
|
||||
const Version JAVA_8(L"1.8"), JAVA_11(L"11");
|
||||
|
||||
const LPCWSTR JDK_OLD = L"SOFTWARE\\JavaSoft\\Java Development Kit";
|
||||
const LPCWSTR JRE_OLD = L"SOFTWARE\\JavaSoft\\Java Runtime Environment";
|
||||
const LPCWSTR JDK_NEW = L"SOFTWARE\\JavaSoft\\JDK";
|
||||
const LPCWSTR JRE_NEW = L"SOFTWARE\\JavaSoft\\JRE";
|
||||
|
||||
bool oldJavaFound = false, newJavaFound = false;
|
||||
|
||||
bool FindJavaByRegistryKey(HKEY rootKey, LPCWSTR subKey, std::wstring & path)
|
||||
{
|
||||
WCHAR javaVer[MAX_KEY_LENGTH]; // buffer for subkey name, special for JavaVersion
|
||||
DWORD cbName; // size of name string
|
||||
DWORD cSubKeys = 0; // number of subkeys
|
||||
DWORD cbMaxSubKey; // longest subkey size
|
||||
DWORD cValues; // number of values for key
|
||||
DWORD cchMaxValue; // longest value name
|
||||
DWORD cbMaxValueData; // longest value data
|
||||
LSTATUS result;
|
||||
|
||||
HKEY hKey;
|
||||
if (ERROR_SUCCESS != (result = RegOpenKeyEx(rootKey, subKey, 0, KEY_WOW64_64KEY | KEY_READ, &hKey)))
|
||||
return false;
|
||||
|
||||
RegQueryInfoKey(
|
||||
hKey, // key handle
|
||||
NULL, // buffer for class name
|
||||
NULL, // size of class string
|
||||
NULL, // reserved
|
||||
&cSubKeys, // number of subkeys
|
||||
&cbMaxSubKey, // longest subkey size
|
||||
NULL, // longest class string
|
||||
&cValues, // number of values for this key
|
||||
&cchMaxValue, // longest value name
|
||||
&cbMaxValueData, // longest value data
|
||||
NULL, // security descriptor
|
||||
NULL); // last write time
|
||||
|
||||
if (!cSubKeys)
|
||||
return false;
|
||||
|
||||
bool flag = false;
|
||||
for (DWORD i = 0; i < cSubKeys; ++i)
|
||||
{
|
||||
cbName = MAX_KEY_LENGTH;
|
||||
if (ERROR_SUCCESS != (result = RegEnumKeyEx(hKey, i, javaVer, &cbName, NULL, NULL, NULL, NULL)))
|
||||
continue;
|
||||
|
||||
HKEY javaKey;
|
||||
if (ERROR_SUCCESS != RegOpenKeyEx(hKey, javaVer, 0, KEY_READ, &javaKey))
|
||||
continue;
|
||||
|
||||
if (ERROR_SUCCESS == MyRegQueryValue(javaKey, L"JavaHome", REG_SZ, path))
|
||||
{
|
||||
if (Version(javaVer) < JAVA_8)
|
||||
oldJavaFound = true;
|
||||
else if (!(Version(javaVer) < JAVA_11))
|
||||
newJavaFound = true;
|
||||
else
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (flag)
|
||||
break;
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
bool FindJavaInRegistry(std::wstring & path)
|
||||
{
|
||||
return FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_OLD, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_OLD, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_NEW, path) ||
|
||||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_NEW, path);
|
||||
}
|
||||
|
||||
bool FindJava(std::wstring & path)
|
||||
{
|
||||
return FindJavaInRegistry(path) ||
|
||||
ERROR_SUCCESS == MyGetEnvironmentVariable(L"JAVA_HOME", path);
|
||||
}
|
||||
10
HMCLauncher/HMCL/java.h
Normal file
10
HMCLauncher/HMCL/java.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include "Version.h"
|
||||
|
||||
// Find Java installation in system registry
|
||||
bool FindJavaInRegistry(std::wstring &path);
|
||||
|
||||
// Find Java Installation in registry and environment variable
|
||||
bool FindJava(std::wstring &path);
|
||||
79
HMCLauncher/HMCL/main.cpp
Normal file
79
HMCLauncher/HMCL/main.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "stdafx.h"
|
||||
#include "main.h"
|
||||
#include "os.h"
|
||||
#include "java.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
void LaunchJVM(const wstring &javaPath, const wstring &jarPath)
|
||||
{
|
||||
if (MyCreateProcess(L"\"" + javaPath + L"\" -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15 -jar \"" + jarPath + L"\""))
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
|
||||
{
|
||||
wstring path, exeName;
|
||||
|
||||
// Since Jar file is appended to this executable, we should first get the location of JAR file.
|
||||
if (ERROR_SUCCESS != MyGetModuleFileName(NULL, exeName))
|
||||
return 1;
|
||||
|
||||
// TODO: check whether the bundled JRE is valid.
|
||||
// First try the Java packaged together.
|
||||
bool is64Bit = false;
|
||||
GetArch(is64Bit); // if failed to determine architecture of operating system, consider 32-bit.
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
LaunchJVM(L"jre-x64\\bin\\javaw.exe", exeName);
|
||||
}
|
||||
else
|
||||
{
|
||||
LaunchJVM(L"jre-x86\\bin\\javaw.exe", exeName);
|
||||
}
|
||||
|
||||
if (FindJava(path))
|
||||
LaunchJVM(path + L"\\bin\\javaw.exe", exeName);
|
||||
|
||||
// Try java in PATH
|
||||
LaunchJVM(L"javaw", exeName);
|
||||
|
||||
// Or we try to search Java in C:\Program Files.
|
||||
{
|
||||
WIN32_FIND_DATA data;
|
||||
HANDLE hFind = FindFirstFile(L"C:\\Program Files\\Java\\*", &data); // Search all subdirectory
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
wstring javaw = wstring(L"C:\\Program Files\\Java\\") + data.cFileName + wstring(L"\\bin\\javaw.exe");
|
||||
if (FindFirstFileExists(javaw.c_str(), 0)) {
|
||||
LaunchJVM(javaw, exeName);
|
||||
}
|
||||
} while (FindNextFile(hFind, &data));
|
||||
FindClose(hFind);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider C:\Program Files (x86)\Java
|
||||
{
|
||||
WIN32_FIND_DATA data;
|
||||
HANDLE hFind = FindFirstFile(L"C:\\Program Files (x86)\\Java\\*", &data); // Search all subdirectory
|
||||
|
||||
if (hFind != INVALID_HANDLE_VALUE) {
|
||||
do {
|
||||
wstring javaw = wstring(L"C:\\Program Files (x86)\\Java\\") + data.cFileName + L"\\bin\\javaw.exe";
|
||||
if (FindFirstFileExists(javaw.c_str(), 0)) {
|
||||
LaunchJVM(javaw, exeName);
|
||||
}
|
||||
} while (FindNextFile(hFind, &data));
|
||||
FindClose(hFind);
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox(NULL, L"Java installation cannot be found in this computer, please download it from https://java.com \n"
|
||||
L"未能在这台电脑上找到Java 8~Java 10,请从 https://java.com 下载安装Java", L"Error", MB_ICONERROR | MB_OK);
|
||||
ShellExecute(0, 0, L"https://java.com/", 0, 0, SW_SHOW);
|
||||
return 1;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user