Account Refactor
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ 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;
|
||||||
|
|
||||||
@@ -40,21 +39,12 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
|||||||
if (account == null) {
|
if (account == null) {
|
||||||
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());
|
setTitle(account.getCharacter());
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ import org.jackhuang.hmcl.auth.Account;
|
|||||||
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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
@@ -56,11 +54,7 @@ public class AccountListItem extends RadioButton {
|
|||||||
title.set(account.getUsername() + " - " + account.getCharacter());
|
title.set(account.getUsername() + " - " + account.getCharacter());
|
||||||
subtitle.set(subtitleString.toString());
|
subtitle.set(subtitleString.toString());
|
||||||
|
|
||||||
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 +63,7 @@ public class AccountListItem extends RadioButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
if (account instanceof YggdrasilAccount) {
|
account.clearCache();
|
||||||
// progressBar.setVisible(true);
|
|
||||||
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
|
|
||||||
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
|
||||||
// progressBar.setVisible(false);
|
|
||||||
|
|
||||||
if (isDependentsSucceeded) {
|
|
||||||
final int scaleRatio = 4;
|
|
||||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
|
||||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove() {
|
public void remove() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ 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;
|
||||||
@@ -39,23 +38,20 @@ 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 org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.emptyList;
|
||||||
import static java.util.Collections.unmodifiableList;
|
import static java.util.Collections.unmodifiableList;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
@@ -89,7 +85,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(
|
||||||
@@ -268,24 +264,11 @@ 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))
|
for (GameProfile profile : profiles) {
|
||||||
return CharacterSelector.DEFAULT.select(account, names);
|
|
||||||
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
|
|
||||||
|
|
||||||
for (GameProfile profile : names) {
|
|
||||||
Image image;
|
|
||||||
final int scaleRatio = 4;
|
|
||||||
try {
|
|
||||||
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
|
|
||||||
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageView portraitView = new ImageView();
|
ImageView portraitView = new ImageView();
|
||||||
portraitView.setSmooth(false);
|
portraitView.setSmooth(false);
|
||||||
portraitView.setImage(AccountHelper.getHead(image, scaleRatio));
|
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||||
FXUtils.limitSize(portraitView, 32, 32);
|
FXUtils.limitSize(portraitView, 32, 32);
|
||||||
|
|
||||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||||
@@ -302,11 +285,11 @@ public class AddAccountPane extends StackPane {
|
|||||||
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()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ 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;
|
||||||
|
|
||||||
@@ -143,16 +144,22 @@ public class AuthlibInjectorServer implements Observable {
|
|||||||
private transient Map<String, String> links = emptyMap();
|
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);
|
||||||
}
|
}
|
||||||
@@ -222,6 +229,10 @@ public class AuthlibInjectorServer implements Observable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,60 @@
|
|||||||
*/
|
*/
|
||||||
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.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);
|
||||||
|
} else {
|
||||||
|
session = acquiredSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
characterUUID = session.getSelectedProfile().getId();
|
||||||
|
authenticated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,22 +83,19 @@ 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 {
|
||||||
try {
|
try {
|
||||||
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
|
session = 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 +103,79 @@ public class YggdrasilAccount extends Account {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 AuthenticationException("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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +32,8 @@ 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(() -> {
|
||||||
synchronized (action) {
|
synchronized (action) {
|
||||||
action.accept(arg.get());
|
action.accept(arg.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
package org.jackhuang.hmcl.util;
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
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;
|
||||||
@@ -172,6 +175,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());
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user