Account Refactor

This commit is contained in:
yushijinhun
2019-02-04 16:40:51 +08:00
parent 93f943d40c
commit 9298f5e030
36 changed files with 961 additions and 583 deletions

View File

@@ -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);
}
}

View 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));
}
}
// ====
}

View File

@@ -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) {

View File

@@ -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));
});
}
} }
} }
}; };

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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()));
} }

View File

@@ -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);

View File

@@ -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));
} }
} }

View File

@@ -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));
} }

View File

@@ -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;
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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;
} }

View File

@@ -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);
} }
} }

View File

@@ -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;
}
} }

View File

@@ -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();

View File

@@ -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)

View File

@@ -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");
}
}

View File

@@ -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");
} }
} }

View File

@@ -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";
}
} }

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
} }
} }

View File

@@ -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;
}
}

View File

@@ -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.");
} }
} }

View File

@@ -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

View File

@@ -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());
} }
} }

View File

@@ -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();

View File

@@ -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();
} }

View File

@@ -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());
} }

View File

@@ -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());

View File

@@ -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));

View File

@@ -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;
}
}
} }

View File

@@ -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);
}
}

View File

@@ -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;
} }

View File

@@ -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);
}
}