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) {
|
public void changeDirectory(File newDirectory) {
|
||||||
setBaseDirectory(newDirectory);
|
setBaseDirectory(newDirectory);
|
||||||
refreshVersionsAsync().start();
|
refreshVersionsAsync().start();
|
||||||
@@ -168,6 +161,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
|||||||
return new Image("file:" + iconFile.getAbsolutePath());
|
return new Image("file:" + iconFile.getAbsolutePath());
|
||||||
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
|
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
|
||||||
return new Image("/assets/img/furnace.png");
|
return new Image("/assets/img/furnace.png");
|
||||||
|
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
|
||||||
|
return new Image("/assets/img/furnace.png");
|
||||||
else
|
else
|
||||||
return new Image("/assets/img/grass.png");
|
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.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
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.YggdrasilAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -63,7 +62,7 @@ public final class Accounts {
|
|||||||
private Accounts() {}
|
private Accounts() {}
|
||||||
|
|
||||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
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);
|
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
|
||||||
|
|
||||||
// ==== login type / account factory mapping ====
|
// ==== login type / account factory mapping ====
|
||||||
@@ -71,7 +70,7 @@ public final class Accounts {
|
|||||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||||
static {
|
static {
|
||||||
type2factory.put("offline", FACTORY_OFFLINE);
|
type2factory.put("offline", FACTORY_OFFLINE);
|
||||||
type2factory.put("yggdrasil", FACTORY_YGGDRASIL);
|
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||||
|
|
||||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||||
@@ -94,7 +93,7 @@ public final class Accounts {
|
|||||||
else if (account instanceof AuthlibInjectorAccount)
|
else if (account instanceof AuthlibInjectorAccount)
|
||||||
return FACTORY_AUTHLIB_INJECTOR;
|
return FACTORY_AUTHLIB_INJECTOR;
|
||||||
else if (account instanceof YggdrasilAccount)
|
else if (account instanceof YggdrasilAccount)
|
||||||
return FACTORY_YGGDRASIL;
|
return FACTORY_MOJANG;
|
||||||
else
|
else
|
||||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||||
}
|
}
|
||||||
@@ -279,7 +278,7 @@ public final class Accounts {
|
|||||||
// ==== Login type name i18n ===
|
// ==== Login type name i18n ===
|
||||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
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"));
|
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
|
||||||
|
|
||||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||||
|
|||||||
@@ -234,8 +234,20 @@ public final class FXUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void installTooltip(Node node, String tooltip) {
|
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||||
installTooltip(node, 0, 5000, 0, new 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) {
|
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.JFXButton;
|
||||||
import com.jfoenix.controls.JFXScrollPane;
|
import com.jfoenix.controls.JFXScrollPane;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
@@ -36,6 +37,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
|||||||
super(skinnable);
|
super(skinnable);
|
||||||
|
|
||||||
SpinnerPane spinnerPane = new SpinnerPane();
|
SpinnerPane spinnerPane = new SpinnerPane();
|
||||||
|
Pane placeholder = new Pane();
|
||||||
|
|
||||||
StackPane contentPane = new StackPane();
|
StackPane contentPane = new StackPane();
|
||||||
{
|
{
|
||||||
@@ -48,9 +50,12 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
|
|||||||
list.setSpacing(10);
|
list.setSpacing(10);
|
||||||
list.setPadding(new Insets(10));
|
list.setPadding(new Insets(10));
|
||||||
|
|
||||||
|
VBox content = new VBox();
|
||||||
|
content.getChildren().setAll(list, placeholder);
|
||||||
|
|
||||||
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
|
||||||
|
|
||||||
scrollPane.setContent(list);
|
scrollPane.setContent(content);
|
||||||
JFXScrollPane.smoothScrolling(scrollPane);
|
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());
|
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));
|
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||||
|
|
||||||
TwoLineListItem prompt = new TwoLineListItem();
|
TwoLineListItem prompt = new TwoLineListItem();
|
||||||
prompt.setTitleFill(Color.WHITE);
|
|
||||||
prompt.setSubtitleFill(Color.WHITE);
|
|
||||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||||
prompt.setPickOnBounds(false);
|
prompt.setPickOnBounds(false);
|
||||||
prompt.setStyle("-jfx-title-font-weight: BOLD;");
|
|
||||||
prompt.titleProperty().bind(latestVersionProperty());
|
prompt.titleProperty().bind(latestVersionProperty());
|
||||||
|
|
||||||
hBox.getChildren().setAll(lblIcon, prompt);
|
hBox.getChildren().setAll(lblIcon, prompt);
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
|||||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||||
|
|
||||||
// ==== Update ====
|
// ==== Update ====
|
||||||
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
|
||||||
updateListener = any -> {
|
updateListener = any -> {
|
||||||
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
||||||
|
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ public abstract class SettingsView extends StackPane {
|
|||||||
HBox hBox = new HBox();
|
HBox hBox = new HBox();
|
||||||
hBox.setSpacing(3);
|
hBox.setSpacing(3);
|
||||||
|
|
||||||
cboFont = new FontComboBox(12, false);
|
cboFont = new FontComboBox(12);
|
||||||
txtFontSize = new JFXTextField();
|
txtFontSize = new JFXTextField();
|
||||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||||
|
|||||||
@@ -17,15 +17,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.account;
|
package org.jackhuang.hmcl.ui.account;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
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.setting.Theme;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||||
|
|
||||||
@@ -38,23 +38,15 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
|||||||
protected void invalidated() {
|
protected void invalidated() {
|
||||||
Account account = get();
|
Account account = get();
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
|
titleProperty().unbind();
|
||||||
setTitle(i18n("account.missing"));
|
setTitle(i18n("account.missing"));
|
||||||
setSubtitle(i18n("account.missing.add"));
|
setSubtitle(i18n("account.missing.add"));
|
||||||
|
imageProperty().unbind();
|
||||||
setImage(new Image("/assets/img/craft_table.png"));
|
setImage(new Image("/assets/img/craft_table.png"));
|
||||||
} else {
|
} else {
|
||||||
setTitle(account.getCharacter());
|
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
|
||||||
setSubtitle(accountSubtitle(account));
|
setSubtitle(accountSubtitle(account));
|
||||||
|
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,21 +17,28 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.account;
|
package org.jackhuang.hmcl.ui.account;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.StringBinding;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.scene.control.RadioButton;
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
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.AuthlibInjectorAccount;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||||
import org.jackhuang.hmcl.game.AccountHelper;
|
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public class AccountListItem extends RadioButton {
|
public class AccountListItem extends RadioButton {
|
||||||
|
|
||||||
private final Account account;
|
private final Account account;
|
||||||
@@ -44,23 +51,24 @@ public class AccountListItem extends RadioButton {
|
|||||||
getStyleClass().clear();
|
getStyleClass().clear();
|
||||||
setUserData(account);
|
setUserData(account);
|
||||||
|
|
||||||
StringBuilder subtitleString = new StringBuilder(Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)));
|
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||||
if (account instanceof AuthlibInjectorAccount) {
|
if (account instanceof AuthlibInjectorAccount) {
|
||||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
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)
|
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
||||||
title.set(account.getCharacter());
|
if (account instanceof OfflineAccount) {
|
||||||
else
|
title.bind(characterName);
|
||||||
title.set(account.getUsername() + " - " + account.getCharacter());
|
} else {
|
||||||
subtitle.set(subtitleString.toString());
|
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||||
|
}
|
||||||
|
|
||||||
final int scaleRatio = 4;
|
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||||
Image image = account instanceof YggdrasilAccount ?
|
|
||||||
AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) :
|
|
||||||
AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
|
|
||||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,19 +77,20 @@ public class AccountListItem extends RadioButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
if (account instanceof YggdrasilAccount) {
|
account.clearCache();
|
||||||
// progressBar.setVisible(true);
|
thread(() -> {
|
||||||
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
|
try {
|
||||||
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
account.logIn();
|
||||||
// progressBar.setVisible(false);
|
} catch (CredentialExpiredException e) {
|
||||||
|
try {
|
||||||
if (isDependentsSucceeded) {
|
DialogController.logIn(account);
|
||||||
final int scaleRatio = 4;
|
} catch (Exception e1) {
|
||||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
|
||||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
}
|
||||||
}
|
} catch (AuthenticationException e) {
|
||||||
}).start();
|
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove() {
|
public void remove() {
|
||||||
|
|||||||
@@ -21,18 +21,26 @@ import com.jfoenix.concurrency.JFXUtilities;
|
|||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXRadioButton;
|
import com.jfoenix.controls.JFXRadioButton;
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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 class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||||
|
|
||||||
public AccountListItemSkin(AccountListItem skinnable) {
|
public AccountListItemSkin(AccountListItem skinnable) {
|
||||||
@@ -58,8 +66,22 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
FXUtils.limitSize(imageView, 32, 32);
|
FXUtils.limitSize(imageView, 32, 32);
|
||||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
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);
|
BorderPane.setAlignment(item, Pos.CENTER);
|
||||||
|
|
||||||
center.getChildren().setAll(imageView, item);
|
center.getChildren().setAll(imageView, item);
|
||||||
root.setCenter(center);
|
root.setCenter(center);
|
||||||
|
|
||||||
@@ -69,7 +91,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
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);
|
right.getChildren().add(btnRefresh);
|
||||||
|
|
||||||
JFXButton btnRemove = new JFXButton();
|
JFXButton btnRemove = new JFXButton();
|
||||||
@@ -77,14 +99,12 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
|||||||
btnRemove.getStyleClass().add("toggle-icon4");
|
btnRemove.getStyleClass().add("toggle-icon4");
|
||||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||||
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
|
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);
|
right.getChildren().add(btnRemove);
|
||||||
root.setRight(right);
|
root.setRight(right);
|
||||||
|
|
||||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||||
JFXDepthManager.setDepth(root, 1);
|
JFXDepthManager.setDepth(root, 1);
|
||||||
item.titleProperty().bind(skinnable.titleProperty());
|
|
||||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
|
||||||
|
|
||||||
getChildren().setAll(root);
|
getChildren().setAll(root);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ public class AccountLoginPane extends StackPane {
|
|||||||
private final Consumer<AuthInfo> success;
|
private final Consumer<AuthInfo> success;
|
||||||
private final Runnable failed;
|
private final Runnable failed;
|
||||||
|
|
||||||
@FXML
|
@FXML private Label lblUsername;
|
||||||
private Label lblUsername;
|
|
||||||
@FXML private JFXPasswordField txtPassword;
|
@FXML private JFXPasswordField txtPassword;
|
||||||
@FXML private Label lblCreationWarning;
|
@FXML private Label lblCreationWarning;
|
||||||
@FXML private JFXProgressBar progressBar;
|
@FXML private JFXProgressBar progressBar;
|
||||||
|
|||||||
@@ -19,13 +19,17 @@ package org.jackhuang.hmcl.ui.account;
|
|||||||
|
|
||||||
import com.jfoenix.concurrency.JFXUtilities;
|
import com.jfoenix.concurrency.JFXUtilities;
|
||||||
import com.jfoenix.controls.*;
|
import com.jfoenix.controls.*;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.ListProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.Hyperlink;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
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.authlibinjector.AuthlibInjectorServer;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||||
import org.jackhuang.hmcl.game.AccountHelper;
|
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CountDownLatch;
|
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 java.util.Objects.requireNonNull;
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||||
@@ -63,12 +69,13 @@ public class AddAccountPane extends StackPane {
|
|||||||
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||||
@FXML private Label lblInjectorServer;
|
@FXML private Label lblInjectorServer;
|
||||||
@FXML private Hyperlink linkManageInjectorServers;
|
|
||||||
@FXML private JFXDialogLayout layout;
|
|
||||||
@FXML private JFXButton btnAccept;
|
@FXML private JFXButton btnAccept;
|
||||||
@FXML private JFXButton btnAddServer;
|
@FXML private JFXButton btnAddServer;
|
||||||
@FXML private JFXButton btnManageServer;
|
@FXML private JFXButton btnManageServer;
|
||||||
@FXML private SpinnerPane acceptPane;
|
@FXML private SpinnerPane acceptPane;
|
||||||
|
@FXML private HBox linksContainer;
|
||||||
|
|
||||||
|
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
|
||||||
|
|
||||||
public AddAccountPane() {
|
public AddAccountPane() {
|
||||||
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
|
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
|
||||||
@@ -79,7 +86,7 @@ public class AddAccountPane extends StackPane {
|
|||||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||||
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));
|
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||||
// try selecting the preferred login type
|
// try selecting the preferred login type
|
||||||
cboType.getSelectionModel().select(
|
cboType.getSelectionModel().select(
|
||||||
@@ -117,6 +124,34 @@ public class AddAccountPane extends StackPane {
|
|||||||
txtUsername.textProperty(),
|
txtUsername.textProperty(),
|
||||||
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
||||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.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 final CountDownLatch latch = new CountDownLatch(1);
|
||||||
private GameProfile selectedProfile = null;
|
private GameProfile selectedProfile = null;
|
||||||
|
|
||||||
{
|
public Selector() {
|
||||||
setStyle("-fx-padding: 8px;");
|
setStyle("-fx-padding: 8px;");
|
||||||
|
|
||||||
cancel.setText(i18n("button.cancel"));
|
cancel.setText(i18n("button.cancel"));
|
||||||
@@ -230,45 +265,33 @@ public class AddAccountPane extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||||
if (!(account instanceof YggdrasilAccount))
|
Platform.runLater(() -> {
|
||||||
return CharacterSelector.DEFAULT.select(account, names);
|
for (GameProfile profile : profiles) {
|
||||||
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
|
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) {
|
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||||
Image image;
|
accountItem.setOnMouseClicked(e -> {
|
||||||
final int scaleRatio = 4;
|
selectedProfile = profile;
|
||||||
try {
|
latch.countDown();
|
||||||
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio);
|
});
|
||||||
} catch (Exception e) {
|
listBox.add(accountItem);
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
|
|
||||||
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
|
|
||||||
}
|
}
|
||||||
|
Controllers.dialog(this);
|
||||||
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));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
latch.await();
|
latch.await();
|
||||||
|
|
||||||
if (selectedProfile == null)
|
if (selectedProfile == null)
|
||||||
throw new NoSelectedCharacterException(account);
|
throw new NoSelectedCharacterException();
|
||||||
|
|
||||||
return selectedProfile;
|
return selectedProfile;
|
||||||
} catch (InterruptedException ignore) {
|
} catch (InterruptedException ignore) {
|
||||||
throw new NoSelectedCharacterException(account);
|
throw new NoSelectedCharacterException();
|
||||||
} finally {
|
} finally {
|
||||||
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
|
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
|
||||||
}
|
}
|
||||||
@@ -296,6 +319,8 @@ public class AddAccountPane extends StackPane {
|
|||||||
return exception.getMessage();
|
return exception.getMessage();
|
||||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||||
return i18n("account.failed.injector_download_failure");
|
return i18n("account.failed.injector_download_failure");
|
||||||
|
} else if (exception instanceof CharacterDeletedException) {
|
||||||
|
return i18n("account.failed.character_deleted");
|
||||||
} else if (exception.getClass() == AuthenticationException.class) {
|
} else if (exception.getClass() == AuthenticationException.class) {
|
||||||
return exception.getLocalizedMessage();
|
return exception.getLocalizedMessage();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.account;
|
|||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
import com.jfoenix.effects.JFXDepthManager;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
@@ -33,17 +35,17 @@ public final class AuthlibInjectorServerItem extends BorderPane {
|
|||||||
private final AuthlibInjectorServer server;
|
private final AuthlibInjectorServer server;
|
||||||
|
|
||||||
private final Label lblServerName = new Label();
|
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) {
|
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
|
||||||
lblServerName.setStyle("-fx-font-size: 15;");
|
lblServerName.setStyle("-fx-font-size: 15;");
|
||||||
lblServerIp.setStyle("-fx-font-size: 10;");
|
lblServerUrl.setStyle("-fx-font-size: 10;");
|
||||||
|
|
||||||
VBox center = new VBox();
|
VBox center = new VBox();
|
||||||
BorderPane.setAlignment(center, Pos.CENTER);
|
BorderPane.setAlignment(center, Pos.CENTER);
|
||||||
center.getChildren().addAll(lblServerName, lblServerIp);
|
center.getChildren().addAll(lblServerName, lblServerUrl);
|
||||||
setCenter(center);
|
setCenter(center);
|
||||||
|
|
||||||
JFXButton right = new JFXButton();
|
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;");
|
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||||
JFXDepthManager.setDepth(this, 1);
|
JFXDepthManager.setDepth(this, 1);
|
||||||
lblServerName.setText(server.getName());
|
lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
|
||||||
lblServerIp.setText(server.getUrl());
|
lblServerUrl.setText(server.getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthlibInjectorServer getServer() {
|
public AuthlibInjectorServer getServer() {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
|
|||||||
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
|
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
|
||||||
right.getStyleClass().add("toggle-icon4");
|
right.getStyleClass().add("toggle-icon4");
|
||||||
right.setOnMouseClicked(e -> onExplore());
|
right.setOnMouseClicked(e -> onExplore());
|
||||||
FXUtils.installTooltip(right, i18n("button.edit"));
|
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||||
setRight(right);
|
setRight(right);
|
||||||
|
|
||||||
Tooltip tip = new Tooltip();
|
Tooltip tip = new Tooltip();
|
||||||
|
|||||||
@@ -24,27 +24,27 @@ import static javafx.collections.FXCollections.singletonObservableList;
|
|||||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXComboBox;
|
import com.jfoenix.controls.JFXComboBox;
|
||||||
|
import com.jfoenix.controls.JFXListCell;
|
||||||
|
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
|
|
||||||
public class FontComboBox extends JFXComboBox<String> {
|
public class FontComboBox extends JFXComboBox<String> {
|
||||||
|
|
||||||
private boolean loaded = false;
|
private boolean loaded = false;
|
||||||
|
|
||||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
|
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
|
||||||
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
|
|
||||||
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
||||||
|
|
||||||
setCellFactory(listView -> new ListCell<String>() {
|
setCellFactory(listView -> new JFXListCell<String>() {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(String item, boolean empty) {
|
public void updateItem(String item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
if (item != null) {
|
if (!empty) {
|
||||||
setText(item);
|
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) {
|
public IconedMenuItem addTooltip(String tooltip) {
|
||||||
FXUtils.installTooltip(this, tooltip);
|
FXUtils.installFastTooltip(this, tooltip);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public final class ImagePickerItem extends BorderPane {
|
|||||||
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
|
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
|
||||||
deleteButton.getStyleClass().add("toggle-icon4");
|
deleteButton.getStyleClass().add("toggle-icon4");
|
||||||
|
|
||||||
FXUtils.installTooltip(selectButton, i18n("button.edit"));
|
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
|
||||||
|
|
||||||
HBox hBox = new HBox();
|
HBox hBox = new HBox();
|
||||||
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
|
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
|
||||||
|
|||||||
@@ -17,15 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.construct;
|
package org.jackhuang.hmcl.ui.construct;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXListView;
|
|
||||||
import com.jfoenix.controls.JFXProgressBar;
|
import com.jfoenix.controls.JFXProgressBar;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListCell;
|
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||||
@@ -45,16 +41,15 @@ import java.util.Map;
|
|||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class TaskListPane extends StackPane {
|
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 Map<Task, ProgressListNode> nodes = new HashMap<>();
|
||||||
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
||||||
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
||||||
|
|
||||||
public TaskListPane() {
|
public TaskListPane() {
|
||||||
getChildren().setAll(listBox);
|
listBox.setSpacing(0);
|
||||||
|
|
||||||
listBox.setPadding(Insets.EMPTY);
|
getChildren().setAll(listBox);
|
||||||
listBox.setCellFactory(listView -> new ProgressListNode());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
||||||
@@ -70,7 +65,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
listBox.getItems().clear();
|
listBox.clear();
|
||||||
finishedTasks.set(0);
|
finishedTasks.set(0);
|
||||||
totTasks.set(0);
|
totTasks.set(0);
|
||||||
});
|
});
|
||||||
@@ -112,66 +107,64 @@ public final class TaskListPane extends StackPane {
|
|||||||
task.setName(i18n("modpack.scan"));
|
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
|
@Override
|
||||||
public void onFinished(Task task) {
|
public void onFinished(Task task) {
|
||||||
|
ProgressListNode node = nodes.remove(task);
|
||||||
|
if (node == null)
|
||||||
|
return;
|
||||||
|
node.unbind();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (listBox.getItems().remove(task))
|
listBox.remove(node);
|
||||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
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 static class ProgressListNode extends BorderPane {
|
||||||
private final BorderPane borderPane = new BorderPane();
|
|
||||||
private final JFXProgressBar bar = new JFXProgressBar();
|
private final JFXProgressBar bar = new JFXProgressBar();
|
||||||
private final Label title = new Label();
|
private final Label title = new Label();
|
||||||
private final Label state = new Label();
|
private final Label state = new Label();
|
||||||
|
|
||||||
{
|
public ProgressListNode(Task task) {
|
||||||
borderPane.setLeft(title);
|
bar.progressProperty().bind(task.progressProperty());
|
||||||
borderPane.setRight(state);
|
title.setText(task.getName());
|
||||||
borderPane.setBottom(bar);
|
state.textProperty().bind(task.messageProperty());
|
||||||
borderPane.setMinWidth(0);
|
|
||||||
borderPane.setPrefWidth(1);
|
|
||||||
|
|
||||||
setPadding(Insets.EMPTY);
|
setLeft(title);
|
||||||
|
setRight(state);
|
||||||
|
setBottom(bar);
|
||||||
|
|
||||||
bar.minWidthProperty().bind(widthProperty());
|
bar.minWidthProperty().bind(widthProperty());
|
||||||
bar.prefWidthProperty().bind(widthProperty());
|
bar.prefWidthProperty().bind(widthProperty());
|
||||||
bar.maxWidthProperty().bind(widthProperty());
|
bar.maxWidthProperty().bind(widthProperty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void unbind() {
|
||||||
protected void updateItem(Task item, boolean empty) {
|
bar.progressProperty().unbind();
|
||||||
boolean wasEmpty = isEmpty();
|
state.textProperty().unbind();
|
||||||
Task oldTask = getItem();
|
}
|
||||||
|
|
||||||
if (!wasEmpty && oldTask != null) {
|
public void setThrowable(Throwable throwable) {
|
||||||
bar.progressProperty().unbind();
|
unbind();
|
||||||
state.textProperty().unbind();
|
state.setText(throwable.getLocalizedMessage());
|
||||||
}
|
bar.setProgress(0);
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,32 +19,15 @@ package org.jackhuang.hmcl.ui.construct;
|
|||||||
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
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.control.Label;
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.layout.VBox;
|
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 VBox {
|
||||||
|
|
||||||
public class TwoLineListItem extends StackPane {
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||||
|
|
||||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
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) {
|
public TwoLineListItem(String titleString, String subtitleString) {
|
||||||
this();
|
this();
|
||||||
|
|
||||||
@@ -55,19 +38,14 @@ public class TwoLineListItem extends StackPane {
|
|||||||
public TwoLineListItem() {
|
public TwoLineListItem() {
|
||||||
setMouseTransparent(true);
|
setMouseTransparent(true);
|
||||||
Label lblTitle = new Label();
|
Label lblTitle = new Label();
|
||||||
lblTitle.textFillProperty().bind(titleFill);
|
lblTitle.getStyleClass().add("title");
|
||||||
lblTitle.fontProperty().bind(titleFont);
|
|
||||||
lblTitle.textProperty().bind(title);
|
lblTitle.textProperty().bind(title);
|
||||||
|
|
||||||
Label lblSubtitle = new Label();
|
Label lblSubtitle = new Label();
|
||||||
lblSubtitle.textFillProperty().bind(subtitleFill);
|
lblSubtitle.getStyleClass().add("subtitle");
|
||||||
lblSubtitle.fontProperty().bind(subtitleFont);
|
|
||||||
lblSubtitle.textProperty().bind(subtitle);
|
lblSubtitle.textProperty().bind(subtitle);
|
||||||
|
|
||||||
VBox vbox = new VBox();
|
getChildren().setAll(lblTitle, lblSubtitle);
|
||||||
vbox.getChildren().setAll(lblTitle, lblSubtitle);
|
|
||||||
getChildren().setAll(vbox);
|
|
||||||
|
|
||||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,74 +73,8 @@ public class TwoLineListItem extends StackPane {
|
|||||||
this.subtitle.set(subtitle);
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getTitle();
|
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;
|
package org.jackhuang.hmcl.ui.download;
|
||||||
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||||
import org.jackhuang.hmcl.mod.CurseCompletionException;
|
import org.jackhuang.hmcl.mod.CurseCompletionException;
|
||||||
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
|
||||||
@@ -111,6 +112,8 @@ public class ModpackInstallWizardProvider implements WizardProvider {
|
|||||||
} else {
|
} else {
|
||||||
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
|
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) {
|
} else if (exception instanceof DownloadException) {
|
||||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import javafx.scene.Node;
|
|||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
import org.jackhuang.hmcl.download.GameBuilder;
|
import org.jackhuang.hmcl.download.GameBuilder;
|
||||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||||
|
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.task.DownloadException;
|
import org.jackhuang.hmcl.task.DownloadException;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
@@ -73,7 +74,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
|
|||||||
settings.put("failure_callback", new FailureCallback() {
|
settings.put("failure_callback", new FailureCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
|
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);
|
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
|
||||||
} else {
|
} else {
|
||||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class DatapackListItem extends BorderPane {
|
|||||||
setCenter(modItem);
|
setCenter(modItem);
|
||||||
|
|
||||||
JFXButton btnRemove = new JFXButton();
|
JFXButton btnRemove = new JFXButton();
|
||||||
FXUtils.installTooltip(btnRemove, i18n("datapack.remove"));
|
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
|
||||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||||
btnRemove.getStyleClass().add("toggle-icon4");
|
btnRemove.getStyleClass().add("toggle-icon4");
|
||||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
|||||||
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
|
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
|
||||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||||
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
|
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);
|
right.getChildren().add(btnUpgrade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public final class ModItem extends BorderPane {
|
|||||||
|
|
||||||
JFXButton btnRemove = new JFXButton();
|
JFXButton btnRemove = new JFXButton();
|
||||||
JFXUtilities.runInFX(() -> {
|
JFXUtilities.runInFX(() -> {
|
||||||
FXUtils.installTooltip(btnRemove, i18n("mods.remove"));
|
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
|
||||||
});
|
});
|
||||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||||
btnRemove.getStyleClass().add("toggle-icon4");
|
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"))
|
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.installFastTooltip(btnDelete, i18n("version.manage.remove"));
|
||||||
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
FXUtils.installFastTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
||||||
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
|
FXUtils.installFastTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||||
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
|
FXUtils.installFastTooltip(btnExport, i18n("modpack.export"));
|
||||||
|
|
||||||
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
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);
|
setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||||
|
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
import org.jackhuang.hmcl.setting.Profiles;
|
import org.jackhuang.hmcl.setting.Profiles;
|
||||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||||
@@ -60,6 +61,7 @@ import java.util.List;
|
|||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage {
|
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 advancedSettingsPane;
|
||||||
@FXML private ComponentList componentList;
|
@FXML private ComponentList componentList;
|
||||||
@FXML private ComponentList iconPickerItemWrapper;
|
@FXML private ComponentList iconPickerItemWrapper;
|
||||||
@FXML private JFXComboBox<?> cboLauncherVisibility;
|
@FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||||
@FXML private JFXCheckBox chkFullscreen;
|
@FXML private JFXCheckBox chkFullscreen;
|
||||||
@FXML private Label lblPhysicalMemory;
|
@FXML private Label lblPhysicalMemory;
|
||||||
@FXML private JFXToggleButton chkNoJVMArgs;
|
@FXML private JFXToggleButton chkNoJVMArgs;
|
||||||
@@ -100,6 +102,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
|
|
||||||
public VersionSettingsPage() {
|
public VersionSettingsPage() {
|
||||||
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
|
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
|
@FXML
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ public class Versions {
|
|||||||
|
|
||||||
public static void updateGameAssets(Profile profile, String version) {
|
public static void updateGameAssets(Profile profile, String version) {
|
||||||
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
|
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
|
||||||
TaskExecutor executor = new GameAssetIndexDownloadTask(profile.getDependency(), resolvedVersion)
|
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
|
||||||
.then(new GameAssetDownloadTask(profile.getDependency(), resolvedVersion))
|
|
||||||
.executor();
|
.executor();
|
||||||
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
|
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
|
||||||
executor.start();
|
executor.start();
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
|||||||
"-- System Details --\n" +
|
"-- System Details --\n" +
|
||||||
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\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);
|
LOG.log(Level.SEVERE, text);
|
||||||
|
|
||||||
|
|||||||
@@ -104,20 +104,24 @@
|
|||||||
-fx-padding: 4 0 4 0;
|
-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 {
|
.bubble {
|
||||||
-fx-background-color: gray;
|
-fx-background-color: gray;
|
||||||
-fx-background-radius: 2px;
|
-fx-background-radius: 2px;
|
||||||
|
-fx-text-fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble .two-line-list-item {
|
.bubble .two-line-list-item > .title,
|
||||||
-jfx-title-fill: white;
|
.bubble .two-line-list-item > .subtitle {
|
||||||
-jfx-subtitle-fill: white;
|
-fx-text-fill: white;
|
||||||
}
|
|
||||||
|
|
||||||
.two-line-list-item {
|
|
||||||
-jfx-title-font-size: 15px;
|
|
||||||
-jfx-title-fill: black;
|
|
||||||
-jfx-subtitle-fill: gray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-title-bar .separator {
|
.window-title-bar .separator {
|
||||||
@@ -338,7 +342,6 @@
|
|||||||
|
|
||||||
.jfx-tool-bar HBox {
|
.jfx-tool-bar HBox {
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
/* -fx-spacing: 25.0;*/
|
|
||||||
-fx-padding: 0.0 5.0;
|
-fx-padding: 0.0 5.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,33 +446,6 @@
|
|||||||
-fx-fill: -fx-base-check-color;
|
-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 *
|
* JFX Slider *
|
||||||
@@ -510,18 +486,12 @@
|
|||||||
-fx-font-size: 10.0;
|
-fx-font-size: 10.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************/
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
* JFX Rippler *
|
* JFX Rippler *
|
||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
/*.jfx-rippler {
|
|
||||||
-fx-rippler-fill: -fx-base-color;
|
|
||||||
-fx-mask-type: RECT;
|
|
||||||
}*/
|
|
||||||
.jfx-rippler:hover {
|
.jfx-rippler:hover {
|
||||||
-fx-cursor: hand;
|
-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 {
|
.jfx-button-raised {
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-background-color: -fx-base-color;
|
-fx-background-color: -fx-base-color;
|
||||||
-fx-font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-button-raised .jfx-rippler {
|
.jfx-button-raised, .jfx-button-raised * {
|
||||||
-jfx-rippler-fill: white;
|
-fx-text-fill: -fx-base-text-fill;
|
||||||
}
|
|
||||||
|
|
||||||
.jfx-button-raised .label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-font-size: 14px;
|
-fx-font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-button-border {
|
.jfx-button-border {
|
||||||
-fx-text-fill: -fx-base-color;
|
|
||||||
-fx-border-color: gray;
|
-fx-border-color: gray;
|
||||||
-fx-border-radius: 5px;
|
-fx-border-radius: 5px;
|
||||||
-fx-border-width: 0.2px;
|
-fx-border-width: 0.2px;
|
||||||
-fx-padding: 8px;
|
-fx-padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-button-border .jfx-rippler {
|
.jfx-button-border, .jfx-button-border * {
|
||||||
-jfx-rippler-fill: -fx-base-check-color;
|
-fx-text-fill: -fx-base-darker-color;
|
||||||
}
|
|
||||||
|
|
||||||
.jfx-button-border .label {
|
|
||||||
-fx-text-fill: -fx-base-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jfx-button-raised-round {
|
.jfx-button-raised-round {
|
||||||
@@ -606,20 +538,6 @@
|
|||||||
-jfx-checked-color: -fx-base-check-color;
|
-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 *
|
* JFX Progress Bar *
|
||||||
@@ -643,15 +561,6 @@
|
|||||||
-fx-background-color: -fx-base-check-color;
|
-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 *
|
* JFX Textfield *
|
||||||
@@ -678,20 +587,12 @@
|
|||||||
* *
|
* *
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|
||||||
.jfx-list-cell:odd,
|
.jfx-list-cell, .list-cell {
|
||||||
.jfx-list-cell:even,
|
|
||||||
.list-cell:odd,
|
|
||||||
.list-cell:even {
|
|
||||||
-fx-background-color: WHITE;
|
-fx-background-color: WHITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-cell:selected, .jfx-list-cell:selected {
|
.list-cell:selected, .jfx-list-cell:selected,
|
||||||
-fx-background-insets: 0.0;
|
.list-cell:hover, .jfx-list-cell:hover {
|
||||||
-fx-text-fill: BLACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jfx-list-cell:filled:hover,
|
|
||||||
.jfx-list-cell:selected .label {
|
|
||||||
-fx-text-fill: black;
|
-fx-text-fill: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,47 +613,6 @@
|
|||||||
-jfx-expanded: false;
|
-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 {
|
.options-list {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
|
||||||
@@ -802,20 +662,6 @@
|
|||||||
-jfx-mask-type: CIRCLE;
|
-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 *
|
* JFX SUBLIST IMPORTANT *
|
||||||
@@ -855,10 +701,6 @@
|
|||||||
-fx-padding: 0 0 0 12;
|
-fx-padding: 0 0 0 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*.custom-jfx-list-view .sublist-container {
|
|
||||||
-fx-padding : 0 0 5 0;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
* JFX Toggle Button *
|
* JFX Toggle Button *
|
||||||
@@ -869,14 +711,6 @@
|
|||||||
-jfx-toggle-color: -fx-base-check-color;
|
-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 {
|
.toggle-label {
|
||||||
-fx-font-size: 14.0px;
|
-fx-font-size: 14.0px;
|
||||||
}
|
}
|
||||||
@@ -1109,31 +943,10 @@
|
|||||||
-fx-fill: #D34336;
|
-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 {
|
.combo-box-popup .list-view .jfx-list-cell {
|
||||||
-fx-background-insets: 0.0;
|
-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 {
|
.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
|
||||||
-jfx-rippler-fill: -fx-base-color;
|
-jfx-rippler-fill: -fx-base-color;
|
||||||
}
|
}
|
||||||
@@ -1387,12 +1200,3 @@
|
|||||||
.fit-width {
|
.fit-width {
|
||||||
-fx-pref-width: 100%;
|
-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"/>
|
<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 GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||||
|
<HBox fx:id="linksContainer" alignment="CENTER_LEFT"/>
|
||||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||||
<graphic>
|
<graphic>
|
||||||
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||||
|
|||||||
@@ -60,16 +60,7 @@
|
|||||||
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
|
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
|
||||||
</left>
|
</left>
|
||||||
<right>
|
<right>
|
||||||
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300">
|
<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>
|
|
||||||
</right>
|
</right>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ account.character=character
|
|||||||
account.choose=Choose a character
|
account.choose=Choose a character
|
||||||
account.create=Create a new account
|
account.create=Create a new account
|
||||||
account.email=Email
|
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_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.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.
|
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=Manage authentication servers
|
||||||
account.injector.manage.title=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.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=Auth Server
|
||||||
account.injector.server_url=Server URL
|
account.injector.server_url=Server URL
|
||||||
account.injector.server_name=Server Name
|
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.jvm_args=Java VM Arguments
|
||||||
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
|
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=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_visibility.keep=Keep the launcher visible.
|
||||||
settings.advanced.launcher_visible=Launcher Visibility
|
settings.advanced.launcher_visible=Launcher Visibility
|
||||||
settings.advanced.minecraft_arguments=Minecraft Arguments
|
settings.advanced.minecraft_arguments=Minecraft Arguments
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ account.character=角色
|
|||||||
account.choose=選擇一個角色
|
account.choose=選擇一個角色
|
||||||
account.create=建立帳戶
|
account.create=建立帳戶
|
||||||
account.email=電子信箱
|
account.email=電子信箱
|
||||||
|
account.failed.character_deleted=此角色已被刪除
|
||||||
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
|
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
|
||||||
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
|
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
|
||||||
account.failed.injector_download_failure=無法下載 authlib-injector,請檢查網路或嘗試切換下載源
|
account.failed.injector_download_failure=無法下載 authlib-injector,請檢查網路或嘗試切換下載源
|
||||||
@@ -48,6 +49,7 @@ account.injector.empty=無(點擊右側加號添加)
|
|||||||
account.injector.manage=管理認證伺服器
|
account.injector.manage=管理認證伺服器
|
||||||
account.injector.manage.title=認證伺服器
|
account.injector.manage.title=認證伺服器
|
||||||
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
||||||
|
account.injector.link.register=註冊
|
||||||
account.injector.server=認證伺服器
|
account.injector.server=認證伺服器
|
||||||
account.injector.server_url=伺服器位址
|
account.injector.server_url=伺服器位址
|
||||||
account.injector.server_name=伺服器名稱
|
account.injector.server_name=伺服器名稱
|
||||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(
|
|||||||
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
|
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
|
||||||
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
|
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
|
||||||
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
|
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_visibility.keep=不隱藏啟動器
|
||||||
settings.advanced.launcher_visible=啟動器可見性
|
settings.advanced.launcher_visible=啟動器可見性
|
||||||
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
|
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ account.character=角色
|
|||||||
account.choose=选择一个角色
|
account.choose=选择一个角色
|
||||||
account.create=新建账户
|
account.create=新建账户
|
||||||
account.email=邮箱
|
account.email=邮箱
|
||||||
|
account.failed.character_deleted=此角色已被删除
|
||||||
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
|
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
|
||||||
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
||||||
account.failed.injector_download_failure=无法下载 authlib-injector,请检查网络或尝试切换下载源
|
account.failed.injector_download_failure=无法下载 authlib-injector,请检查网络或尝试切换下载源
|
||||||
@@ -48,6 +49,7 @@ account.injector.empty=无(点击右侧加号添加)
|
|||||||
account.injector.manage=管理认证服务器
|
account.injector.manage=管理认证服务器
|
||||||
account.injector.manage.title=认证服务器
|
account.injector.manage.title=认证服务器
|
||||||
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
||||||
|
account.injector.link.register=注册
|
||||||
account.injector.server=认证服务器
|
account.injector.server=认证服务器
|
||||||
account.injector.server_url=服务器地址
|
account.injector.server_url=服务器地址
|
||||||
account.injector.server_name=服务器名称
|
account.injector.server_name=服务器名称
|
||||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=内存永久保存区域(不
|
|||||||
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
|
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
|
||||||
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
|
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
|
||||||
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
|
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_visibility.keep=保持启动器可见
|
||||||
settings.advanced.launcher_visible=启动器可见性
|
settings.advanced.launcher_visible=启动器可见性
|
||||||
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
|
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
|
|||||||
|
|
||||||
public abstract Map<Object, Object> toStorage();
|
public abstract Map<Object, Object> toStorage();
|
||||||
|
|
||||||
public abstract void clearCache();
|
public void clearCache() {
|
||||||
|
}
|
||||||
|
|
||||||
private ObservableHelper helper = new ObservableHelper(this);
|
private ObservableHelper helper = new ObservableHelper(this);
|
||||||
|
|
||||||
|
|||||||
@@ -17,27 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.auth;
|
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 {
|
public final class CharacterDeletedException extends AuthenticationException {
|
||||||
private UUID uuid;
|
public CharacterDeletedException() {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.auth;
|
package org.jackhuang.hmcl.auth;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||||
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||||
|
|
||||||
import java.util.List;
|
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.
|
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
|
||||||
* @return your choice of game profile.
|
* @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.)
|
* (A account may hold more than one characters.)
|
||||||
*/
|
*/
|
||||||
public final class NoCharacterException extends AuthenticationException {
|
public final class NoCharacterException extends AuthenticationException {
|
||||||
private final Account account;
|
public NoCharacterException() {
|
||||||
|
|
||||||
public NoCharacterException(Account account) {
|
|
||||||
this.account = account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Account getAccount() {
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
|
|||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public final class NoSelectedCharacterException extends AuthenticationException {
|
public final class NoSelectedCharacterException extends AuthenticationException {
|
||||||
private final Account account;
|
public NoSelectedCharacterException() {
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param account the error yggdrasil account.
|
|
||||||
*/
|
|
||||||
public NoSelectedCharacterException(Account account) {
|
|
||||||
this.account = account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Account getAccount() {
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
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.auth.yggdrasil.YggdrasilSession;
|
||||||
import org.jackhuang.hmcl.game.Arguments;
|
import org.jackhuang.hmcl.game.Arguments;
|
||||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||||
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
|
|||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||||
private AuthlibInjectorServer server;
|
private final AuthlibInjectorServer server;
|
||||||
private AuthlibInjectorArtifactProvider downloader;
|
private AuthlibInjectorArtifactProvider downloader;
|
||||||
|
|
||||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
|
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||||
super(service, username, characterUUID, session);
|
super(server.getYggdrasilService(), username, password, selector);
|
||||||
|
|
||||||
this.downloader = downloader;
|
|
||||||
this.server = server;
|
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
|
@Override
|
||||||
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||||
return inject(() -> super.logInWithPassword(password, selector));
|
return inject(() -> super.logInWithPassword(password));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -111,7 +115,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
return new Arguments().addJVMArguments(
|
return new Arguments().addJVMArguments(
|
||||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||||
"-Dauthlibinjector.side=client",
|
"-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
|
@Override
|
||||||
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearCache() {
|
||||||
|
super.clearCache();
|
||||||
|
server.invalidateMetadataCache();
|
||||||
|
}
|
||||||
|
|
||||||
public AuthlibInjectorServer getServer() {
|
public AuthlibInjectorServer getServer() {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
|
|||||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
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 org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
|||||||
|
|
||||||
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
||||||
|
|
||||||
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
|
||||||
server, downloader, username, null, null);
|
|
||||||
account.logInWithPassword(password, selector);
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
|||||||
|
|
||||||
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
||||||
|
|
||||||
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||||
server, downloader, username, session.getSelectedProfile().getId(), session);
|
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")
|
@SerializedName("build_number")
|
||||||
public int buildNumber;
|
public int buildNumber;
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
|
|||||||
public URL getProfilePropertiesURL(UUID uuid) {
|
public URL getProfilePropertiesURL(UUID uuid) {
|
||||||
return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(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;
|
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
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.Lang.tryCast;
|
||||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
|
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
|
||||||
@@ -29,9 +30,12 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -137,18 +141,25 @@ public class AuthlibInjectorServer implements Observable {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private transient String name;
|
private transient String name;
|
||||||
|
private transient Map<String, String> links = emptyMap();
|
||||||
|
|
||||||
private transient boolean metadataRefreshed;
|
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) {
|
public AuthlibInjectorServer(String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public YggdrasilService getYggdrasilService() {
|
||||||
|
return yggdrasilService;
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<String> getMetadataResponse() {
|
public Optional<String> getMetadataResponse() {
|
||||||
return Optional.ofNullable(metadataResponse);
|
return Optional.ofNullable(metadataResponse);
|
||||||
}
|
}
|
||||||
@@ -162,6 +173,10 @@ public class AuthlibInjectorServer implements Observable {
|
|||||||
.orElse(url);
|
.orElse(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getLinks() {
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
public String fetchMetadataResponse() throws IOException {
|
public String fetchMetadataResponse() throws IOException {
|
||||||
if (metadataResponse == null || !metadataRefreshed) {
|
if (metadataResponse == null || !metadataRefreshed) {
|
||||||
refreshMetadata();
|
refreshMetadata();
|
||||||
@@ -201,9 +216,23 @@ public class AuthlibInjectorServer implements Observable {
|
|||||||
|
|
||||||
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
|
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
|
||||||
.orElse(null);
|
.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
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return url.hashCode();
|
return url.hashCode();
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
|
|||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
|
|||||||
private final String username;
|
private final String username;
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
|
|
||||||
OfflineAccount(String username, UUID uuid) {
|
protected OfflineAccount(String username, UUID uuid) {
|
||||||
Objects.requireNonNull(username);
|
this.username = requireNonNull(username);
|
||||||
Objects.requireNonNull(uuid);
|
this.uuid = requireNonNull(uuid);
|
||||||
|
|
||||||
this.username = username;
|
if (StringUtils.isBlank(username)) {
|
||||||
this.uuid = uuid;
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(username))
|
|
||||||
throw new IllegalArgumentException("Username cannot be blank");
|
throw new IllegalArgumentException("Username cannot be blank");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthInfo logIn() throws AuthenticationException {
|
public AuthInfo logIn() {
|
||||||
if (StringUtils.isBlank(username))
|
|
||||||
throw new AuthenticationException("Username cannot be empty");
|
|
||||||
|
|
||||||
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<AuthInfo> playOffline() {
|
public Optional<AuthInfo> playOffline() {
|
||||||
return Optional.empty();
|
return Optional.of(logIn());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearCache() {
|
|
||||||
// Nothing to clear.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this)
|
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;
|
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import static java.util.Objects.requireNonNull;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
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
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class GameProfile implements Validation {
|
public class GameProfile implements Validation {
|
||||||
|
|
||||||
|
@JsonAdapter(UUIDTypeAdapter.class)
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
private final String name;
|
|
||||||
private final PropertyMap properties;
|
|
||||||
|
|
||||||
public GameProfile() {
|
private final String name;
|
||||||
this(null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameProfile(UUID id, String name) {
|
public GameProfile(UUID id, String name) {
|
||||||
this(id, name, new PropertyMap());
|
this.id = requireNonNull(id);
|
||||||
}
|
this.name = requireNonNull(name);
|
||||||
|
|
||||||
public GameProfile(UUID id, String name, PropertyMap properties) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
public UUID getId() {
|
||||||
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return nullable
|
|
||||||
*/
|
|
||||||
public PropertyMap getProperties() {
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate() throws JsonParseException {
|
public void validate() throws JsonParseException {
|
||||||
if (id == null)
|
if (id == null)
|
||||||
throw new JsonParseException("Game profile id cannot be null or malformed");
|
throw new JsonParseException("Game profile id cannot be null");
|
||||||
if (StringUtils.isBlank(name))
|
if (name == null)
|
||||||
throw new JsonParseException("Game profile name cannot be null or blank");
|
throw new JsonParseException("Game profile name cannot be null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import java.net.URL;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||||
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getAuthenticationURL() {
|
public URL getAuthenticationURL() {
|
||||||
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
|
|||||||
public URL getProfilePropertiesURL(UUID uuid) {
|
public URL getProfilePropertiesURL(UUID uuid) {
|
||||||
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(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;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMetadata(String key) {
|
public Map<String, String> getMetadata() {
|
||||||
if (metadata == null)
|
return metadata;
|
||||||
return null;
|
|
||||||
else
|
|
||||||
return metadata.get(key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
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.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
import org.jackhuang.hmcl.util.gson.Validation;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author huang
|
* @author huang
|
||||||
*/
|
*/
|
||||||
|
@Immutable
|
||||||
public final class User implements Validation {
|
public final class User implements Validation {
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final PropertyMap properties;
|
|
||||||
|
@Nullable
|
||||||
|
private final Map<String, String> properties;
|
||||||
|
|
||||||
public User(String id) {
|
public User(String id) {
|
||||||
this(id, null);
|
this(id, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User(String id, PropertyMap properties) {
|
public User(String id, Map<String, String> properties) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
@@ -43,7 +51,7 @@ public final class User implements Validation {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PropertyMap getProperties() {
|
public Map<String, String> getProperties() {
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,5 +60,4 @@ public final class User implements Validation {
|
|||||||
if (StringUtils.isBlank(id))
|
if (StringUtils.isBlank(id))
|
||||||
throw new JsonParseException("User id cannot be empty.");
|
throw new JsonParseException("User id cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,32 +17,62 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.auth.*;
|
import static java.util.Objects.requireNonNull;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
|
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 org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author huangyuhui
|
|
||||||
*/
|
|
||||||
public class YggdrasilAccount extends Account {
|
public class YggdrasilAccount extends Account {
|
||||||
|
|
||||||
private final String username;
|
|
||||||
private final YggdrasilService service;
|
private final YggdrasilService service;
|
||||||
private boolean isOnline = false;
|
private final UUID characterUUID;
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
private boolean authenticated = false;
|
||||||
private YggdrasilSession session;
|
private YggdrasilSession session;
|
||||||
private UUID characterUUID;
|
|
||||||
|
|
||||||
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
|
protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
|
||||||
this.service = service;
|
this.service = requireNonNull(service);
|
||||||
this.username = username;
|
this.username = requireNonNull(username);
|
||||||
this.session = session;
|
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
|
||||||
this.characterUUID = characterUUID;
|
this.session = requireNonNull(session);
|
||||||
|
}
|
||||||
|
|
||||||
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
|
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||||
this.session = null;
|
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
|
@Override
|
||||||
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
|
|||||||
return session.getSelectedProfile().getName();
|
return session.getSelectedProfile().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLoggedIn() {
|
@Override
|
||||||
return session != null && StringUtils.isNotBlank(session.getAccessToken());
|
public UUID getUUID() {
|
||||||
}
|
return session.getSelectedProfile().getId();
|
||||||
|
|
||||||
public boolean canPlayOnline() {
|
|
||||||
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||||
if (!canPlayOnline()) {
|
if (!authenticated) {
|
||||||
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||||
isOnline = true;
|
authenticated = true;
|
||||||
} else {
|
} else {
|
||||||
|
YggdrasilSession acquiredSession;
|
||||||
try {
|
try {
|
||||||
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
|
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||||
} catch (RemoteAuthenticationException e) {
|
} catch (RemoteAuthenticationException e) {
|
||||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||||
throw new CredentialExpiredException(e);
|
throw new CredentialExpiredException(e);
|
||||||
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
|
|||||||
throw e;
|
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();
|
return session.toAuthInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
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.getSelectedProfile() == null) {
|
||||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
|
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||||
throw new NoCharacterException(this);
|
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.getAccessToken(),
|
||||||
acquiredSession.getClientToken(),
|
acquiredSession.getClientToken(),
|
||||||
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
|
characterToSelect);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.session = acquiredSession;
|
if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||||
|
throw new CharacterDeletedException();
|
||||||
|
}
|
||||||
|
session = acquiredSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.characterUUID = this.session.getSelectedProfile().getId();
|
authenticated = true;
|
||||||
invalidate();
|
invalidate();
|
||||||
|
return session.toAuthInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<AuthInfo> playOffline() {
|
public Optional<AuthInfo> playOffline() {
|
||||||
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
|
return Optional.of(session.toAuthInfo());
|
||||||
return Optional.of(session.toAuthInfo());
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Object, Object> toStorage() {
|
public Map<Object, Object> toStorage() {
|
||||||
if (session == null)
|
Map<Object, Object> storage = new HashMap<>();
|
||||||
throw new IllegalStateException("No session is specified");
|
storage.put("username", username);
|
||||||
|
|
||||||
HashMap<Object, Object> storage = new HashMap<>();
|
|
||||||
storage.put("username", getUsername());
|
|
||||||
storage.putAll(session.toStorage());
|
storage.putAll(session.toStorage());
|
||||||
|
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
|
||||||
|
storage.put("profileProperties", profile.getProperties());
|
||||||
|
});
|
||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public YggdrasilService getYggdrasilService() {
|
||||||
public UUID getUUID() {
|
return service;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
Optional.ofNullable(session)
|
authenticated = false;
|
||||||
.map(YggdrasilSession::getSelectedProfile)
|
service.getProfileRepository().invalidate(characterUUID);
|
||||||
.map(GameProfile::getProperties)
|
}
|
||||||
.ifPresent(it -> it.remove("textures"));
|
|
||||||
|
private static String randomClientToken() {
|
||||||
|
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "YggdrasilAccount[username=" + getUsername() + "]";
|
return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
|
|||||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
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> {
|
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||||
|
|
||||||
private final YggdrasilProvider provider;
|
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
|
||||||
|
|
||||||
public YggdrasilAccountFactory(YggdrasilProvider provider) {
|
private YggdrasilService service;
|
||||||
this.provider = provider;
|
|
||||||
|
public YggdrasilAccountFactory(YggdrasilService service) {
|
||||||
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
|||||||
Objects.requireNonNull(username);
|
Objects.requireNonNull(username);
|
||||||
Objects.requireNonNull(password);
|
Objects.requireNonNull(password);
|
||||||
|
|
||||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null);
|
return new YggdrasilAccount(service, username, password, selector);
|
||||||
account.logInWithPassword(password, selector);
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
|||||||
String username = tryCast(storage.get("username"), String.class)
|
String username = tryCast(storage.get("username"), String.class)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
.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 new YggdrasilAccount(service, username, session);
|
||||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
|
|||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.*;
|
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.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.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;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
public class YggdrasilService {
|
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 YggdrasilProvider provider;
|
||||||
|
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||||
|
|
||||||
public YggdrasilService(YggdrasilProvider provider) {
|
public YggdrasilService(YggdrasilProvider provider) {
|
||||||
this.provider = 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 {
|
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);
|
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<>();
|
Map<String, Object> request = new HashMap<>();
|
||||||
request.put("accessToken", accessToken);
|
request.put("accessToken", accessToken);
|
||||||
request.put("clientToken", clientToken);
|
request.put("clientToken", clientToken);
|
||||||
@@ -82,7 +105,16 @@ public class YggdrasilService {
|
|||||||
pair("name", characterToSelect.getName())));
|
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 {
|
public boolean validate(String accessToken) throws AuthenticationException {
|
||||||
@@ -121,20 +153,19 @@ public class YggdrasilService {
|
|||||||
* @param uuid the uuid that the character corresponding to.
|
* @param uuid the uuid that the character corresponding to.
|
||||||
* @return the complete game profile(filled with more properties)
|
* @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);
|
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);
|
Objects.requireNonNull(profile);
|
||||||
|
|
||||||
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
|
String encodedTextures = profile.getProperties().get("textures");
|
||||||
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
|
|
||||||
|
|
||||||
if (encodedTextures.isPresent()) {
|
if (encodedTextures != null) {
|
||||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
|
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
|
||||||
return Optional.ofNullable(texturePayload.textures);
|
return Optional.ofNullable(texturePayload.textures);
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -148,7 +179,12 @@ public class YggdrasilService {
|
|||||||
if (!clientToken.equals(response.clientToken))
|
if (!clientToken.equals(response.clientToken))
|
||||||
throw new AuthenticationException("Client token changed from " + clientToken + " to " + 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 {
|
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 {
|
try {
|
||||||
if (payload == null)
|
if (payload == null)
|
||||||
return NetworkUtils.doGet(url);
|
return NetworkUtils.doGet(url);
|
||||||
@@ -187,26 +223,25 @@ public class YggdrasilService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextureResponse {
|
private static class TextureResponse {
|
||||||
public Map<TextureType, Texture> textures;
|
public Map<TextureType, Texture> textures;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AuthenticationResponse extends ErrorResponse {
|
private static class AuthenticationResponse extends ErrorResponse {
|
||||||
public String accessToken;
|
public String accessToken;
|
||||||
public String clientToken;
|
public String clientToken;
|
||||||
public GameProfile selectedProfile;
|
public GameProfile selectedProfile;
|
||||||
public GameProfile[] availableProfiles;
|
public List<GameProfile> availableProfiles;
|
||||||
public User user;
|
public User user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ErrorResponse {
|
private static class ErrorResponse {
|
||||||
public String error;
|
public String error;
|
||||||
public String errorMessage;
|
public String errorMessage;
|
||||||
public String cause;
|
public String cause;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = new GsonBuilder()
|
||||||
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
|
|
||||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||||
.create();
|
.create();
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||||
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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.Lang.tryCast;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
public class YggdrasilSession {
|
public class YggdrasilSession {
|
||||||
|
|
||||||
private String clientToken;
|
private final String clientToken;
|
||||||
private String accessToken;
|
private final String accessToken;
|
||||||
private GameProfile selectedProfile;
|
private final GameProfile selectedProfile;
|
||||||
private GameProfile[] availableProfiles;
|
private final List<GameProfile> availableProfiles;
|
||||||
private User user;
|
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.clientToken = clientToken;
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
this.selectedProfile = selectedProfile;
|
this.selectedProfile = selectedProfile;
|
||||||
@@ -64,7 +66,7 @@ public class YggdrasilSession {
|
|||||||
/**
|
/**
|
||||||
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
||||||
*/
|
*/
|
||||||
public GameProfile[] getAvailableProfiles() {
|
public List<GameProfile> getAvailableProfiles() {
|
||||||
return availableProfiles;
|
return availableProfiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ public class YggdrasilSession {
|
|||||||
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
||||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
String 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"));
|
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));
|
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("{}"));
|
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
|
return baseURL
|
||||||
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||||
.replace("https://launcher.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")
|
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
|
.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/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
|
||||||
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
|
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
|
||||||
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
|
.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.ForgeInstallTask;
|
||||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
import org.jackhuang.hmcl.download.game.*;
|
||||||
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.liteloader.LiteLoaderInstallTask;
|
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||||
@@ -74,7 +71,13 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
|||||||
@Override
|
@Override
|
||||||
public Task checkGameCompletionAsync(Version version) {
|
public Task checkGameCompletionAsync(Version version) {
|
||||||
return new ParallelTask(
|
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)
|
new GameLibrariesTask(this, version)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
|||||||
version = version.setId(name).setJar(null);
|
version = version.setId(name).setJar(null);
|
||||||
variables.set("version", version);
|
variables.set("version", version);
|
||||||
Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask(
|
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.
|
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.
|
).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) {
|
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);
|
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
||||||
VersionLibraryBuilder builder = new VersionLibraryBuilder(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 dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
|
||||||
* @param version the <b>resolved</b> version
|
* @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.dependencyManager = dependencyManager;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.assetIndexInfo = version.getAssetIndex();
|
this.assetIndexInfo = version.getAssetIndex();
|
||||||
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||||
|
|
||||||
if (!assetIndexFile.exists())
|
if (!assetIndexFile.exists() || forceDownloadingIndex)
|
||||||
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
|
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,5 +99,7 @@ public final class GameAssetDownloadTask extends Task {
|
|||||||
updateProgress(++progress, index.getObjects().size());
|
updateProgress(++progress, index.getObjects().size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final boolean DOWNLOAD_INDEX_FORCIBLY = true;
|
||||||
|
public static final boolean DOWNLOAD_INDEX_IF_NECESSARY = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,14 +55,18 @@ public final class GameDownloadTask extends Task {
|
|||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||||
|
|
||||||
dependencies.add(new FileDownloadTask(
|
FileDownloadTask task = new FileDownloadTask(
|
||||||
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
|
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
|
||||||
jar,
|
jar,
|
||||||
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
|
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
|
||||||
.setCaching(true)
|
.setCaching(true)
|
||||||
.setCacheRepository(dependencyManager.getCacheRepository())
|
.setCacheRepository(dependencyManager.getCacheRepository());
|
||||||
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar")));
|
|
||||||
|
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 AbstractDependencyManager dependencyManager;
|
||||||
private final Version version;
|
private final Version version;
|
||||||
|
private final List<Library> libraries;
|
||||||
private final List<Task> dependencies = new LinkedList<>();
|
private final List<Task> dependencies = new LinkedList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
|
|||||||
* @param version the <b>resolved</b> version
|
* @param version the <b>resolved</b> version
|
||||||
*/
|
*/
|
||||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version 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.dependencyManager = dependencyManager;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
|
this.libraries = libraries;
|
||||||
|
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
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);
|
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||||
if (!file.exists())
|
if (!file.exists())
|
||||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
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 com.google.gson.JsonParseException;
|
||||||
import org.jackhuang.hmcl.event.*;
|
import org.jackhuang.hmcl.event.*;
|
||||||
import org.jackhuang.hmcl.mod.ModManager;
|
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.ToStringBuilder;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of classic Minecraft game repository.
|
* An implementation of classic Minecraft game repository.
|
||||||
*
|
*
|
||||||
@@ -81,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Path getArtifactFile(Version version, Artifact artifact) {
|
||||||
|
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File getRunDirectory(String id) {
|
public File getRunDirectory(String id) {
|
||||||
return getBaseDirectory();
|
return getBaseDirectory();
|
||||||
@@ -112,7 +117,7 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Version readVersionJson(File file) throws IOException, JsonParseException {
|
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
|
@Override
|
||||||
@@ -171,13 +176,13 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
|
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
|
||||||
jsons.forEach(f -> {
|
jsons.forEach(f -> {
|
||||||
if (!f.delete())
|
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.
|
// remove the version from version list regardless of whether the directory was removed successfully or not.
|
||||||
try {
|
try {
|
||||||
FileUtils.deleteDirectory(removedFile);
|
FileUtils.deleteDirectory(removedFile);
|
||||||
} catch (IOException e) {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -202,25 +207,31 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
// we will find the only json and rename it to correct name.
|
// we will find the only json and rename it to correct name.
|
||||||
if (!json.exists()) {
|
if (!json.exists()) {
|
||||||
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
|
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)) {
|
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();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LOG.info("No available json file found, ignoring version " + id);
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Version version;
|
Version version;
|
||||||
try {
|
try {
|
||||||
version = Objects.requireNonNull(readVersionJson(json));
|
version = readVersionJson(json);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LOG.log(Level.WARNING, "Malformed version json " + id, e);
|
||||||
// JsonSyntaxException or IOException or NullPointerException(!!)
|
// JsonSyntaxException or IOException or NullPointerException(!!)
|
||||||
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
|
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
version = Objects.requireNonNull(readVersionJson(json));
|
version = readVersionJson(json);
|
||||||
} catch (Exception e2) {
|
} 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();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +241,7 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
try {
|
try {
|
||||||
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
|
FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
|
||||||
} catch (Exception e) {
|
} 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();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,7 +257,7 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY)
|
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY)
|
||||||
versions.put(version.getId(), version);
|
versions.put(version.getId(), version);
|
||||||
} catch (VersionNotFoundException e) {
|
} 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)
|
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Schedulers.newThread().schedule(() -> {
|
refreshVersionsImpl();
|
||||||
refreshVersionsImpl();
|
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -278,7 +287,7 @@ public class DefaultGameRepository implements GameRepository {
|
|||||||
try {
|
try {
|
||||||
return reconstructAssets(version, assetId);
|
return reconstructAssets(version, assetId);
|
||||||
} catch (IOException | JsonParseException e) {
|
} 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);
|
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.Immutable;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||||
|
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
import org.jackhuang.hmcl.util.gson.Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,8 +75,8 @@ public class DownloadInfo implements Validation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate() throws JsonParseException {
|
public void validate() throws JsonParseException, TolerableValidationException {
|
||||||
if (StringUtils.isBlank(url))
|
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;
|
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.CompressingUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||||
@@ -30,6 +36,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
@@ -37,6 +44,16 @@ import java.util.stream.StreamSupport;
|
|||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public final class GameVersion {
|
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 {
|
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
||||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||||
|
|
||||||
@@ -74,6 +91,13 @@ public final class GameVersion {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
||||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
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");
|
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||||
if (Files.exists(minecraft)) {
|
if (Files.exists(minecraft)) {
|
||||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||||
@@ -88,4 +112,24 @@ public final class GameVersion {
|
|||||||
return Optional.empty();
|
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 com.google.gson.annotations.SerializedName;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -54,7 +55,7 @@ public class IdDownloadInfo extends DownloadInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate() throws JsonParseException {
|
public void validate() throws JsonParseException, TolerableValidationException {
|
||||||
super.validate();
|
super.validate();
|
||||||
|
|
||||||
if (StringUtils.isBlank(id))
|
if (StringUtils.isBlank(id))
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ public class Library implements Comparable<Library> {
|
|||||||
private final String classifier;
|
private final String classifier;
|
||||||
private final String url;
|
private final String url;
|
||||||
private final LibrariesDownloadInfo downloads;
|
private final LibrariesDownloadInfo downloads;
|
||||||
private final LibraryDownloadInfo download;
|
private transient final LibraryDownloadInfo download;
|
||||||
private final ExtractRules extract;
|
private final ExtractRules extract;
|
||||||
private final Map<OperatingSystem, String> natives;
|
private final Map<OperatingSystem, String> natives;
|
||||||
private final List<CompatibilityRule> rules;
|
private final List<CompatibilityRule> rules;
|
||||||
private final List<String> checksums;
|
private final List<String> checksums;
|
||||||
|
|
||||||
private final String path;
|
private transient final String path;
|
||||||
|
|
||||||
public Library(String groupId, String artifactId, String version) {
|
public Library(String groupId, String artifactId, String version) {
|
||||||
this(groupId, artifactId, version, null, null, null);
|
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.JsonParseException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
import org.jackhuang.hmcl.util.gson.Validation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +67,7 @@ public final class LoggingInfo implements Validation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate() throws JsonParseException {
|
public void validate() throws JsonParseException, TolerableValidationException {
|
||||||
file.validate();
|
file.validate();
|
||||||
if (StringUtils.isBlank(argument))
|
if (StringUtils.isBlank(argument))
|
||||||
throw new JsonParseException("LoggingInfo.argument is empty.");
|
throw new JsonParseException("LoggingInfo.argument is empty.");
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +132,7 @@ public final class MultiMCModpackInstallTask extends Task {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
Version version = Objects.requireNonNull(repository.readVersionJson(name));
|
Version version = repository.readVersionJson(name);
|
||||||
|
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
package org.jackhuang.hmcl.task;
|
package org.jackhuang.hmcl.task;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
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.event.EventManager;
|
||||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||||
@@ -55,18 +58,14 @@ public abstract class Task {
|
|||||||
this.significance = significance;
|
this.significance = significance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReadOnlyObjectWrapper<TaskState> state = new ReadOnlyObjectWrapper<>(this, "state", TaskState.READY);
|
private TaskState state = TaskState.READY;
|
||||||
|
|
||||||
public TaskState getState() {
|
public TaskState getState() {
|
||||||
return state.get();
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setState(TaskState state) {
|
void setState(TaskState state) {
|
||||||
this.state.setValue(state);
|
this.state = state;
|
||||||
}
|
|
||||||
|
|
||||||
public ReadOnlyObjectProperty<TaskState> stateProperty() {
|
|
||||||
return state.getReadOnlyProperty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Throwable lastException = null;
|
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) {
|
public static Task of(String name, ExceptionalRunnable<?> runnable) {
|
||||||
return of(name, ExceptionalConsumer.fromRunnable(runnable));
|
return of(name, ExceptionalConsumer.fromRunnable(runnable));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@@ -31,9 +32,11 @@ import java.util.function.Supplier;
|
|||||||
*/
|
*/
|
||||||
public class InvocationDispatcher<ARG> implements Consumer<ARG> {
|
public class InvocationDispatcher<ARG> implements Consumer<ARG> {
|
||||||
|
|
||||||
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) {
|
public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
|
||||||
return new InvocationDispatcher<>(arg -> executor.accept(() -> {
|
return new InvocationDispatcher<>(arg -> executor.execute(() -> {
|
||||||
action.accept(arg.get());
|
synchronized (action) {
|
||||||
|
action.accept(arg.get());
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util;
|
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.ExceptionalRunnable;
|
||||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
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
|
* @author huangyuhui
|
||||||
@@ -172,6 +176,17 @@ public final class Lang {
|
|||||||
return thread;
|
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) {
|
public static int parseInt(Object string, int defaultValue) {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(string.toString());
|
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.
|
* Find the first non-null reference in given list.
|
||||||
* @param t nullable references list.
|
* @param t nullable references list.
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -24,8 +26,6 @@ import java.util.LinkedList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
@@ -128,6 +128,10 @@ public final class StringUtils {
|
|||||||
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
|
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) {
|
public static String removeSurrounding(String str, String delimiter) {
|
||||||
return removeSurrounding(str, delimiter, delimiter);
|
return removeSurrounding(str, delimiter, delimiter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
|
|||||||
*/
|
*/
|
||||||
public interface ExceptionalFunction<T, R, E extends Exception> {
|
public interface ExceptionalFunction<T, R, E extends Exception> {
|
||||||
R apply(T t) throws E;
|
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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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?
|
* This exception gets thrown by implementations of {@link Validation#validate()} if you want to replace
|
||||||
* When C inherits from B, and B inherits from something else, and finally inherits from C again.
|
* the nullable JSON-parsed object which does not satisfy the constraint with null value.
|
||||||
*
|
* @see Validation
|
||||||
* @author huangyuhui
|
|
||||||
*/
|
*/
|
||||||
public final class CircleDependencyException extends GameException {
|
public final class TolerableValidationException extends Exception {
|
||||||
|
|
||||||
public CircleDependencyException() {
|
public TolerableValidationException() {
|
||||||
}
|
|
||||||
|
|
||||||
public CircleDependencyException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CircleDependencyException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
|||||||
|
|
||||||
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
|
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
|
||||||
|
|
||||||
private UUIDTypeAdapter() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(JsonWriter writer, UUID value) throws IOException {
|
public void write(JsonWriter writer, UUID value) throws IOException {
|
||||||
writer.value(value == null ? null : fromUUID(value));
|
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.
|
* @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>() {
|
return new TypeAdapter<T>() {
|
||||||
@Override
|
@Override
|
||||||
public void write(JsonWriter writer, T t) throws IOException {
|
public void write(JsonWriter writer, T t) throws IOException {
|
||||||
if (t instanceof Validation)
|
if (t instanceof Validation) {
|
||||||
((Validation) t).validate();
|
try {
|
||||||
|
((Validation) t).validate();
|
||||||
|
} catch (TolerableValidationException e) {
|
||||||
|
delegate.write(writer, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate.write(writer, t);
|
delegate.write(writer, t);
|
||||||
}
|
}
|
||||||
@@ -49,8 +55,13 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
|
|||||||
@Override
|
@Override
|
||||||
public T read(JsonReader reader) throws IOException {
|
public T read(JsonReader reader) throws IOException {
|
||||||
T t = delegate.read(reader);
|
T t = delegate.read(reader);
|
||||||
if (t instanceof Validation)
|
if (t instanceof Validation) {
|
||||||
((Validation) t).validate();
|
try {
|
||||||
|
((Validation) t).validate();
|
||||||
|
} catch (TolerableValidationException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -275,6 +275,22 @@ public final class FileUtils {
|
|||||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
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 {
|
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||||
copyFile(srcFile, destFile);
|
copyFile(srcFile, destFile);
|
||||||
srcFile.delete();
|
srcFile.delete();
|
||||||
|
|||||||
@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
|
|||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
|
||||||
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
|||||||
return new FlatMappedBinding<>(map(mapper), nullAlternative);
|
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> {
|
private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
|
||||||
|
|
||||||
public SimpleBinding(ObservableValue<T> predecessor) {
|
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) {
|
public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
|
||||||
return new MappedBinding<>(predecessor, 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> {
|
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 List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
private Observable source;
|
private Observable source;
|
||||||
|
|
||||||
|
public ObservableHelper() {
|
||||||
|
this.source = this;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableHelper(Observable source) {
|
public ObservableHelper(Observable source) {
|
||||||
this.source = 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
|
* Copied from org.apache.maven.artifact.versioning.ComparableVersion
|
||||||
* Apache License 2.0
|
* Apache License 2.0
|
||||||
|
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
|
||||||
*/
|
*/
|
||||||
public class VersionNumber implements Comparable<VersionNumber> {
|
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>
|
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
|
||||||
<Keyword>Win32Proj</Keyword>
|
<Keyword>Win32Proj</Keyword>
|
||||||
<RootNamespace>HMCL</RootNamespace>
|
<RootNamespace>HMCL</RootNamespace>
|
||||||
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
|
<WindowsTargetPlatformVersion>7.0</WindowsTargetPlatformVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||||
@@ -152,19 +152,25 @@
|
|||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="HMCL.h" />
|
<ClInclude Include="java.h" />
|
||||||
|
<ClInclude Include="main.h" />
|
||||||
|
<ClInclude Include="os.h" />
|
||||||
<ClInclude Include="Resource.h" />
|
<ClInclude Include="Resource.h" />
|
||||||
<ClInclude Include="stdafx.h" />
|
<ClInclude Include="stdafx.h" />
|
||||||
<ClInclude Include="targetver.h" />
|
<ClInclude Include="targetver.h" />
|
||||||
|
<ClInclude Include="Version.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="HMCL.cpp" />
|
<ClCompile Include="java.cpp" />
|
||||||
|
<ClCompile Include="main.cpp" />
|
||||||
|
<ClCompile Include="os.cpp" />
|
||||||
<ClCompile Include="stdafx.cpp">
|
<ClCompile Include="stdafx.cpp">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="Version.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ResourceCompile Include="HMCL.rc" />
|
<ResourceCompile Include="HMCL.rc" />
|
||||||
|
|||||||
@@ -24,7 +24,16 @@
|
|||||||
<ClInclude Include="Resource.h">
|
<ClInclude Include="Resource.h">
|
||||||
<Filter>头文件</Filter>
|
<Filter>头文件</Filter>
|
||||||
</ClInclude>
|
</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>
|
<Filter>头文件</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -32,7 +41,16 @@
|
|||||||
<ClCompile Include="stdafx.cpp">
|
<ClCompile Include="stdafx.cpp">
|
||||||
<Filter>源文件</Filter>
|
<Filter>源文件</Filter>
|
||||||
</ClCompile>
|
</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>
|
<Filter>源文件</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
</ItemGroup>
|
</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