From 9298f5e03038402df415f05d72ca4314bf52d540 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 4 Feb 2019 16:40:51 +0800 Subject: [PATCH] Account Refactor --- .../jackhuang/hmcl/game/AccountHelper.java | 194 ---------------- .../jackhuang/hmcl/game/TexturesLoader.java | 209 ++++++++++++++++++ .../org/jackhuang/hmcl/setting/Accounts.java | 9 +- .../ui/account/AccountAdvancedListItem.java | 16 +- .../hmcl/ui/account/AccountListItem.java | 24 +- .../hmcl/ui/account/AccountLoginPane.java | 3 +- .../hmcl/ui/account/AddAccountPane.java | 33 +-- .../java/org/jackhuang/hmcl/auth/Account.java | 3 +- ...or.java => CharacterDeletedException.java} | 23 +- .../hmcl/auth/CharacterSelector.java | 4 +- .../hmcl/auth/NoCharacterException.java | 9 +- .../auth/NoSelectedCharacterException.java | 13 +- .../AuthlibInjectorAccount.java | 26 ++- .../AuthlibInjectorAccountFactory.java | 19 +- .../AuthlibInjectorProvider.java | 5 + .../AuthlibInjectorServer.java | 13 +- .../hmcl/auth/offline/OfflineAccount.java | 26 +-- .../auth/yggdrasil/CompleteGameProfile.java | 59 +++++ .../hmcl/auth/yggdrasil/GameProfile.java | 44 ++-- .../yggdrasil/MojangYggdrasilProvider.java | 6 +- .../hmcl/auth/yggdrasil/PropertyMap.java | 85 ------- .../auth/yggdrasil/PropertyMapSerializer.java | 60 +++++ .../hmcl/auth/yggdrasil/Texture.java | 7 +- .../hmcl/auth/yggdrasil/TextureModel.java | 43 ++++ .../jackhuang/hmcl/auth/yggdrasil/User.java | 15 +- .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 169 +++++++------- .../yggdrasil/YggdrasilAccountFactory.java | 26 ++- .../hmcl/auth/yggdrasil/YggdrasilService.java | 67 ++++-- .../hmcl/auth/yggdrasil/YggdrasilSession.java | 22 +- .../hmcl/util/InvocationDispatcher.java | 5 +- .../java/org/jackhuang/hmcl/util/Lang.java | 14 ++ .../hmcl/util/gson/UUIDTypeAdapter.java | 3 - .../hmcl/util/javafx/MultiStepBinding.java | 62 ++++++ .../hmcl/util/javafx/ObservableCache.java | 162 ++++++++++++++ .../hmcl/util/javafx/ObservableHelper.java | 4 + .../util/javafx/ObservableOptionalCache.java | 62 ++++++ 36 files changed, 961 insertions(+), 583 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/auth/{SpecificCharacterSelector.java => CharacterDeletedException.java} (55%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/CompleteGameProfile.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMapSerializer.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableCache.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableOptionalCache.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java deleted file mode 100644 index c13d65e4b..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2019 huangyuhui 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 . - */ -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 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 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 = 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 = 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); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java new file mode 100644 index 000000000..f1d967a40 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -0,0 +1,209 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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 metadata; + + public LoadedTexture(BufferedImage image, Map metadata) { + this.image = image; + this.metadata = metadata; + } + + public BufferedImage getImage() { + return image; + } + + public Map 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 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 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 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 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)); + } + } + // ==== +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index dc5ee4405..c2097f290 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -35,7 +35,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; -import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; import org.jackhuang.hmcl.task.Schedulers; @@ -63,7 +62,7 @@ public final class Accounts { private Accounts() {} public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE; - public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE); + public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG; public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer); // ==== login type / account factory mapping ==== @@ -71,7 +70,7 @@ public final class Accounts { private static final Map, String> factory2type = new HashMap<>(); static { type2factory.put("offline", FACTORY_OFFLINE); - type2factory.put("yggdrasil", FACTORY_YGGDRASIL); + type2factory.put("yggdrasil", FACTORY_MOJANG); type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR); type2factory.forEach((type, factory) -> factory2type.put(factory, type)); @@ -94,7 +93,7 @@ public final class Accounts { else if (account instanceof AuthlibInjectorAccount) return FACTORY_AUTHLIB_INJECTOR; else if (account instanceof YggdrasilAccount) - return FACTORY_YGGDRASIL; + return FACTORY_MOJANG; else throw new IllegalArgumentException("Failed to determine account type: " + account); } @@ -279,7 +278,7 @@ public final class Accounts { // ==== Login type name i18n === private static Map, String> unlocalizedLoginTypeNames = mapOf( pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"), - pair(Accounts.FACTORY_YGGDRASIL, "account.methods.yggdrasil"), + pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"), pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector")); public static String getLocalizedLoginTypeName(AccountFactory factory) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java index d521df319..dd6225ab8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountAdvancedListItem.java @@ -23,9 +23,8 @@ import javafx.scene.image.Image; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Theme; -import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; @@ -40,21 +39,12 @@ public class AccountAdvancedListItem extends AdvancedListItem { if (account == null) { setTitle(i18n("account.missing")); setSubtitle(i18n("account.missing.add")); + imageProperty().unbind(); setImage(new Image("/assets/img/craft_table.png")); } else { setTitle(account.getCharacter()); setSubtitle(accountSubtitle(account)); - - final int scaleRatio = 4; - Image defaultSkin = AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio); - setImage(AccountHelper.getHead(defaultSkin, scaleRatio)); - - if (account instanceof YggdrasilAccount) { - AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> { - Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio); - setImage(AccountHelper.getHead(image, scaleRatio)); - }); - } + imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32)); } } }; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java index 867fe8e56..b6f3c82d4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItem.java @@ -25,10 +25,8 @@ import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.offline.OfflineAccount; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.task.Schedulers; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -56,11 +54,7 @@ public class AccountListItem extends RadioButton { title.set(account.getUsername() + " - " + account.getCharacter()); subtitle.set(subtitleString.toString()); - final int scaleRatio = 4; - Image image = account instanceof YggdrasilAccount ? - AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) : - AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio); - this.image.set(AccountHelper.getHead(image, scaleRatio)); + image.bind(TexturesLoader.fxAvatarBinding(account, 32)); } @Override @@ -69,19 +63,7 @@ public class AccountListItem extends RadioButton { } public void refresh() { - if (account instanceof YggdrasilAccount) { - // progressBar.setVisible(true); - AccountHelper.refreshSkinAsync((YggdrasilAccount) account) - .finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> { - // progressBar.setVisible(false); - - if (isDependentsSucceeded) { - final int scaleRatio = 4; - Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio); - this.image.set(AccountHelper.getHead(image, scaleRatio)); - } - }).start(); - } + account.clearCache(); } public void remove() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java index 95ca81e8a..3a758bed5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountLoginPane.java @@ -37,8 +37,7 @@ public class AccountLoginPane extends StackPane { private final Consumer success; private final Runnable failed; - @FXML - private Label lblUsername; + @FXML private Label lblUsername; @FXML private JFXPasswordField txtPassword; @FXML private Label lblCreationWarning; @FXML private JFXProgressBar progressBar; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java index a346b08dc..34567d2fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java @@ -29,7 +29,6 @@ import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; -import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -39,23 +38,20 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.game.AccountHelper; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.game.TexturesLoader; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.javafx.MultiStepBinding; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; -import java.util.logging.Level; - import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; @@ -89,7 +85,7 @@ public class AddAccountPane extends StackPane { cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer)); selectDefaultServer(); - cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR); + cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR); cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName)); // try selecting the preferred login type cboType.getSelectionModel().select( @@ -268,24 +264,11 @@ public class AddAccountPane extends StackPane { } @Override - public GameProfile select(Account account, List names) throws NoSelectedCharacterException { - if (!(account instanceof YggdrasilAccount)) - 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); - } - + public GameProfile select(YggdrasilService service, List profiles) throws NoSelectedCharacterException { + for (GameProfile profile : profiles) { ImageView portraitView = new ImageView(); portraitView.setSmooth(false); - portraitView.setImage(AccountHelper.getHead(image, scaleRatio)); + portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32)); FXUtils.limitSize(portraitView, 32, 32); IconedItem accountItem = new IconedItem(portraitView, profile.getName()); @@ -302,11 +285,11 @@ public class AddAccountPane extends StackPane { latch.await(); if (selectedProfile == null) - throw new NoSelectedCharacterException(account); + throw new NoSelectedCharacterException(); return selectedProfile; } catch (InterruptedException ignore) { - throw new NoSelectedCharacterException(account); + throw new NoSelectedCharacterException(); } finally { JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent())); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index 5d41f50a5..3d0808197 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -69,7 +69,8 @@ public abstract class Account implements Observable { public abstract Map toStorage(); - public abstract void clearCache(); + public void clearCache() { + } private ObservableHelper helper = new ObservableHelper(this); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterDeletedException.java similarity index 55% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterDeletedException.java index b7636db5f..3997df9af 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterDeletedException.java @@ -17,27 +17,10 @@ */ package org.jackhuang.hmcl.auth; -import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; - -import java.util.List; -import java.util.UUID; - /** - * Select character by name. + * Thrown when a previously existing character cannot be found. */ -public class SpecificCharacterSelector implements CharacterSelector { - private UUID uuid; - - /** - * Constructor. - * @param uuid character's uuid. - */ - public SpecificCharacterSelector(UUID uuid) { - this.uuid = uuid; - } - - @Override - public GameProfile select(Account account, List names) throws NoSelectedCharacterException { - return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account)); +public final class CharacterDeletedException extends AuthenticationException { + public CharacterDeletedException() { } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java index 5933338cd..416f08fb6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.auth; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import java.util.List; @@ -33,7 +34,6 @@ public interface CharacterSelector { * @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection. * @return your choice of game profile. */ - GameProfile select(Account account, List names) throws NoSelectedCharacterException; + GameProfile select(YggdrasilService yggdrasilService, List names) throws NoSelectedCharacterException; - CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java index b5938fcd1..30f6aaaaa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoCharacterException.java @@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth; * (A account may hold more than one characters.) */ public final class NoCharacterException extends AuthenticationException { - private final Account account; - - public NoCharacterException(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; + public NoCharacterException() { } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java index eb72a6135..878e5ca61 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java @@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth; * @author huangyuhui */ public final class NoSelectedCharacterException extends AuthenticationException { - private final Account account; - - /** - * - * @param account the error yggdrasil account. - */ - public NoSelectedCharacterException(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; + public NoSelectedCharacterException() { } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java index dafa67343..0e96efdf4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java @@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.util.ToStringBuilder; @@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; public class AuthlibInjectorAccount extends YggdrasilAccount { - private AuthlibInjectorServer server; + private final AuthlibInjectorServer server; private AuthlibInjectorArtifactProvider downloader; - protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) { - super(service, username, characterUUID, session); - - this.downloader = downloader; + public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException { + super(server.getYggdrasilService(), username, password, selector); this.server = server; + this.downloader = downloader; + } + + public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) { + super(server.getYggdrasilService(), username, session); + this.server = server; + this.downloader = downloader; } @Override @@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { } @Override - protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException { - return inject(() -> super.logInWithPassword(password, selector)); + public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException { + return inject(() -> super.logInWithPassword(password)); } @Override @@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return map; } + @Override + public void clearCache() { + super.clearCache(); + server.invalidateMetadataCache(); + } + public AuthlibInjectorServer getServer() { return server; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java index d51dc9771..69c01df30 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java @@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector; import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.CharacterSelector; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile; +import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import java.util.Map; import java.util.Objects; @@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory { + @SuppressWarnings("unchecked") + Map properties = it; + GameProfile selected = session.getSelectedProfile(); + server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties)); + }); + + return new AuthlibInjectorAccount(server, downloader, username, session); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java index 65e75e3ba..ef99cd51b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java @@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider { public URL getProfilePropertiesURL(UUID uuid) { return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); } + + @Override + public String toString() { + return apiRoot; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java index e3d9aff9b..75183438f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Optional; import java.util.logging.Level; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jetbrains.annotations.Nullable; @@ -143,16 +144,22 @@ public class AuthlibInjectorServer implements Observable { private transient Map links = emptyMap(); private transient boolean metadataRefreshed; - private transient ObservableHelper helper = new ObservableHelper(this); + private final transient ObservableHelper helper = new ObservableHelper(this); + private final transient YggdrasilService yggdrasilService; public AuthlibInjectorServer(String url) { this.url = url; + this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url)); } public String getUrl() { return url; } + public YggdrasilService getYggdrasilService() { + return yggdrasilService; + } + public Optional getMetadataResponse() { return Optional.ofNullable(metadataResponse); } @@ -222,6 +229,10 @@ public class AuthlibInjectorServer implements Observable { } } + public void invalidateMetadataCache() { + metadataRefreshed = false; + } + @Override public int hashCode() { return url.hashCode(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index fee167ca8..c1afe5bf6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.UUID; +import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; @@ -41,15 +41,13 @@ public class OfflineAccount extends Account { private final String username; private final UUID uuid; - OfflineAccount(String username, UUID uuid) { - Objects.requireNonNull(username); - Objects.requireNonNull(uuid); + protected OfflineAccount(String username, UUID uuid) { + this.username = requireNonNull(username); + this.uuid = requireNonNull(uuid); - this.username = username; - this.uuid = uuid; - - if (StringUtils.isBlank(username)) + if (StringUtils.isBlank(username)) { throw new IllegalArgumentException("Username cannot be blank"); + } } @Override @@ -68,10 +66,7 @@ public class OfflineAccount extends Account { } @Override - public AuthInfo logIn() throws AuthenticationException { - if (StringUtils.isBlank(username)) - throw new AuthenticationException("Username cannot be empty"); - + public AuthInfo logIn() { return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}"); } @@ -82,7 +77,7 @@ public class OfflineAccount extends Account { @Override public Optional playOffline() { - return Optional.empty(); + return Optional.of(logIn()); } @Override @@ -93,11 +88,6 @@ public class OfflineAccount extends Account { ); } - @Override - public void clearCache() { - // Nothing to clear. - } - @Override public String toString() { return new ToStringBuilder(this) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/CompleteGameProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/CompleteGameProfile.java new file mode 100644 index 000000000..75e3928ba --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/CompleteGameProfile.java @@ -0,0 +1,59 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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 properties; + + public CompleteGameProfile(UUID id, String name, Map properties) { + super(id, name); + this.properties = requireNonNull(properties); + } + + public CompleteGameProfile(GameProfile profile, Map properties) { + this(profile.getId(), profile.getName(), properties); + } + + public Map getProperties() { + return properties; + } + + @Override + public void validate() throws JsonParseException { + super.validate(); + + if (properties == null) + throw new JsonParseException("Game profile properties cannot be null"); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java index 4846cc1f5..8144c8d91 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java @@ -17,36 +17,31 @@ */ package org.jackhuang.hmcl.auth.yggdrasil; -import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.gson.Validation; +import static java.util.Objects.requireNonNull; import java.util.UUID; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; +import org.jackhuang.hmcl.util.gson.Validation; + +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; + /** - * * @author huangyuhui */ @Immutable -public final class GameProfile implements Validation { +public class GameProfile implements Validation { + @JsonAdapter(UUIDTypeAdapter.class) private final UUID id; - private final String name; - private final PropertyMap properties; - public GameProfile() { - this(null, null); - } + private final String name; public GameProfile(UUID id, String name) { - this(id, name, new PropertyMap()); - } - - public GameProfile(UUID id, String name, PropertyMap properties) { - this.id = id; - this.name = name; - this.properties = properties; + this.id = requireNonNull(id); + this.name = requireNonNull(name); } public UUID getId() { @@ -57,18 +52,11 @@ public final class GameProfile implements Validation { return name; } - /** - * @return nullable - */ - public PropertyMap getProperties() { - return properties; - } - @Override public void validate() throws JsonParseException { if (id == null) - throw new JsonParseException("Game profile id cannot be null or malformed"); - if (StringUtils.isBlank(name)) - throw new JsonParseException("Game profile name cannot be null or blank"); + throw new JsonParseException("Game profile id cannot be null"); + if (name == null) + throw new JsonParseException("Game profile name cannot be null"); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java index 736a84fc3..0b3e982b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java @@ -24,7 +24,6 @@ import java.net.URL; import java.util.UUID; public class MojangYggdrasilProvider implements YggdrasilProvider { - public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider(); @Override public URL getAuthenticationURL() { @@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider { public URL getProfilePropertiesURL(UUID uuid) { return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); } + + @Override + public String toString() { + return "mojang"; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java deleted file mode 100644 index 5f7fa732c..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2019 huangyuhui 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 . - */ -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 { - - 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, JsonDeserializer { - - 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 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 { - public static final LegacySerializer INSTANCE = new LegacySerializer(); - - @Override - public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - for (PropertyMap.Entry entry : src.entrySet()) { - JsonArray values = new JsonArray(); - values.add(new JsonPrimitive(entry.getValue())); - result.add(entry.getKey(), values); - } - return result; - } - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMapSerializer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMapSerializer.java new file mode 100644 index 000000000..c9f9b1a2c --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMapSerializer.java @@ -0,0 +1,60 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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>, JsonDeserializer> { + + @Override + public Map deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + Map 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 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; + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java index 1ba19d6f4..54e10b9eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java @@ -40,10 +40,7 @@ public final class Texture { return url; } - public String getMetadata(String key) { - if (metadata == null) - return null; - else - return metadata.get(key); + public Map getMetadata() { + return metadata; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java new file mode 100644 index 000000000..ec233a038 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureModel.java @@ -0,0 +1,43 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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 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; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java index 9f00b19e1..dfff36e5f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/User.java @@ -18,23 +18,31 @@ package org.jackhuang.hmcl.auth.yggdrasil; import com.google.gson.JsonParseException; + +import java.util.Map; + +import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.Validation; +import org.jetbrains.annotations.Nullable; /** * * @author huang */ +@Immutable public final class User implements Validation { private final String id; - private final PropertyMap properties; + + @Nullable + private final Map properties; public User(String id) { this(id, null); } - public User(String id, PropertyMap properties) { + public User(String id, Map properties) { this.id = id; this.properties = properties; } @@ -43,7 +51,7 @@ public final class User implements Validation { return id; } - public PropertyMap getProperties() { + public Map getProperties() { return properties; } @@ -52,5 +60,4 @@ public final class User implements Validation { if (StringUtils.isBlank(id)) throw new JsonParseException("User id cannot be empty."); } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 9ca4ce41f..d99189bdc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -17,32 +17,60 @@ */ package org.jackhuang.hmcl.auth.yggdrasil; -import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.util.StringUtils; +import static java.util.Objects.requireNonNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.CharacterDeletedException; +import org.jackhuang.hmcl.auth.CharacterSelector; +import org.jackhuang.hmcl.auth.CredentialExpiredException; +import org.jackhuang.hmcl.auth.NoCharacterException; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; -import java.util.*; - -/** - * - * @author huangyuhui - */ public class YggdrasilAccount extends Account { - private final String username; private final YggdrasilService service; - private boolean isOnline = false; + private final UUID characterUUID; + private final String username; + + private boolean authenticated = false; private YggdrasilSession session; - private UUID characterUUID; - protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) { - this.service = service; - this.username = username; - this.session = session; - this.characterUUID = characterUUID; + protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) { + this.service = requireNonNull(service); + this.username = requireNonNull(username); + this.characterUUID = requireNonNull(session.getSelectedProfile().getId()); + this.session = requireNonNull(session); + } - if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken())) - this.session = null; + protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException { + this.service = requireNonNull(service); + this.username = requireNonNull(username); + + YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken()); + if (acquiredSession.getSelectedProfile() == null) { + if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) { + throw new NoCharacterException(); + } + + GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles()); + + session = service.refresh( + acquiredSession.getAccessToken(), + acquiredSession.getClientToken(), + characterToSelect); + } else { + session = acquiredSession; + } + + characterUUID = session.getSelectedProfile().getId(); + authenticated = true; } @Override @@ -55,22 +83,19 @@ public class YggdrasilAccount extends Account { return session.getSelectedProfile().getName(); } - public boolean isLoggedIn() { - return session != null && StringUtils.isNotBlank(session.getAccessToken()); - } - - public boolean canPlayOnline() { - return isLoggedIn() && session.getSelectedProfile() != null && isOnline; + @Override + public UUID getUUID() { + return session.getSelectedProfile().getId(); } @Override public synchronized AuthInfo logIn() throws AuthenticationException { - if (!canPlayOnline()) { + if (!authenticated) { if (service.validate(session.getAccessToken(), session.getClientToken())) { - isOnline = true; + authenticated = true; } else { try { - updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID)); + session = service.refresh(session.getAccessToken(), session.getClientToken(), null); } catch (RemoteAuthenticationException e) { if ("ForbiddenOperationException".equals(e.getRemoteName())) { throw new CredentialExpiredException(e); @@ -78,95 +103,79 @@ public class YggdrasilAccount extends Account { throw e; } } + + authenticated = true; + invalidate(); } } + return session.toAuthInfo(); } @Override public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException { - return logInWithPassword(password, new SpecificCharacterSelector(characterUUID)); - } + YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken()); - protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException { - updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector); - return session.toAuthInfo(); - } - - /** - * Updates the current session. This method shall be invoked after authenticate/refresh operation. - * {@link #session} field shall be set only using this method. This method ensures {@link #session} - * has a profile selected. - * - * @param acquiredSession the session acquired by making an authenticate/refresh request - */ - private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException { if (acquiredSession.getSelectedProfile() == null) { - if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0) - throw new NoCharacterException(this); + if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) { + throw new CharacterDeletedException(); + } - this.session = service.refresh( + GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream() + .filter(charatcer -> charatcer.getId().equals(characterUUID)) + .findFirst() + .orElseThrow(CharacterDeletedException::new); + + session = service.refresh( acquiredSession.getAccessToken(), acquiredSession.getClientToken(), - selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles()))); + characterToSelect); + } else { - this.session = acquiredSession; + if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) { + throw new CharacterDeletedException(); + } + session = acquiredSession; } - this.characterUUID = this.session.getSelectedProfile().getId(); + authenticated = true; invalidate(); + return session.toAuthInfo(); } @Override public Optional playOffline() { - if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline()) - return Optional.of(session.toAuthInfo()); - - return Optional.empty(); + return Optional.of(session.toAuthInfo()); } @Override public Map toStorage() { - if (session == null) - throw new IllegalStateException("No session is specified"); - - HashMap storage = new HashMap<>(); - storage.put("username", getUsername()); + Map storage = new HashMap<>(); + storage.put("username", username); storage.putAll(session.toStorage()); + service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> { + storage.put("profileProperties", profile.getProperties()); + }); return storage; } - @Override - public UUID getUUID() { - if (session == null || session.getSelectedProfile() == null) - return null; - else - return session.getSelectedProfile().getId(); - } - - public Optional getSkin() throws AuthenticationException { - return getSkin(session.getSelectedProfile()); - } - - public Optional getSkin(GameProfile profile) throws AuthenticationException { - if (!service.getTextures(profile).isPresent()) { - profile = service.getCompleteGameProfile(profile.getId()).orElse(profile); - } - - return service.getTextures(profile).map(map -> map.get(TextureType.SKIN)); + public YggdrasilService getYggdrasilService() { + return service; } @Override public void clearCache() { - Optional.ofNullable(session) - .map(YggdrasilSession::getSelectedProfile) - .map(GameProfile::getProperties) - .ifPresent(it -> it.remove("textures")); + authenticated = false; + service.getProfileRepository().invalidate(characterUUID); + } + + private static String randomClientToken() { + return UUIDTypeAdapter.fromUUID(UUID.randomUUID()); } @Override public String toString() { - return "YggdrasilAccount[username=" + getUsername() + "]"; + return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]"; } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java index 64ec96885..19b7a9a10 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java @@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil; import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.CharacterSelector; -import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import java.util.Map; import java.util.Objects; -import java.util.UUID; import static org.jackhuang.hmcl.util.Lang.tryCast; @@ -34,10 +32,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast; */ public class YggdrasilAccountFactory extends AccountFactory { - private final YggdrasilProvider provider; + public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG); - public YggdrasilAccountFactory(YggdrasilProvider provider) { - this.provider = provider; + private YggdrasilService service; + + public YggdrasilAccountFactory(YggdrasilService service) { + this.service = service; } @Override @@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory { Objects.requireNonNull(username); Objects.requireNonNull(password); - YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null); - account.logInWithPassword(password, selector); - return account; + return new YggdrasilAccount(service, username, password, selector); } @Override @@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory { String username = tryCast(storage.get("username"), String.class) .orElseThrow(() -> new IllegalArgumentException("storage does not have username")); - return new YggdrasilAccount(new YggdrasilService(provider), username, session.getSelectedProfile().getId(), session); - } + tryCast(storage.get("profileProperties"), Map.class).ifPresent( + it -> { + @SuppressWarnings("unchecked") + Map properties = it; + GameProfile selected = session.getSelectedProfile(); + service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties)); + }); - public static String randomToken() { - return UUIDTypeAdapter.fromUUID(UUID.randomUUID()); + return new YggdrasilAccount(service, username, session); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index 49f5fb695..c3f51fd13 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; import java.io.IOException; import java.net.URL; import java.util.*; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.unmodifiableList; import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Lang.threadPool; +import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; public class YggdrasilService { + private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS); + + public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider()); + private final YggdrasilProvider provider; + private final ObservableOptionalCache profileRepository; public YggdrasilService(YggdrasilProvider provider) { this.provider = provider; + this.profileRepository = new ObservableOptionalCache<>( + uuid -> { + LOG.info("Fetching properties of " + uuid + " from " + provider); + return getCompleteGameProfile(uuid); + }, + (uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid + " from " + provider, e), + POOL); + } + + public ObservableOptionalCache getProfileRepository() { + return profileRepository; } public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException { @@ -62,7 +85,7 @@ public class YggdrasilService { return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken); } - private Map createRequestWithCredentials(String accessToken, String clientToken) { + private static Map createRequestWithCredentials(String accessToken, String clientToken) { Map request = new HashMap<>(); request.put("accessToken", accessToken); request.put("clientToken", clientToken); @@ -82,7 +105,16 @@ public class YggdrasilService { pair("name", characterToSelect.getName()))); } - return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken); + YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken); + + if (characterToSelect != null) { + if (response.getSelectedProfile() == null || + !response.getSelectedProfile().getId().equals(characterToSelect.getId())) { + throw new AuthenticationException("Failed to select character"); + } + } + + return response; } public boolean validate(String accessToken) throws AuthenticationException { @@ -121,20 +153,19 @@ public class YggdrasilService { * @param uuid the uuid that the character corresponding to. * @return the complete game profile(filled with more properties) */ - public Optional getCompleteGameProfile(UUID uuid) throws AuthenticationException { + public Optional getCompleteGameProfile(UUID uuid) throws AuthenticationException { Objects.requireNonNull(uuid); - return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class)); + return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), CompleteGameProfile.class)); } - public Optional> getTextures(GameProfile profile) throws AuthenticationException { + public static Optional> getTextures(CompleteGameProfile profile) throws ServerResponseMalformedException { Objects.requireNonNull(profile); - Optional encodedTextures = Optional.ofNullable(profile.getProperties()) - .flatMap(properties -> Optional.ofNullable(properties.get("textures"))); + String encodedTextures = profile.getProperties().get("textures"); - if (encodedTextures.isPresent()) { - TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class); + if (encodedTextures != null) { + TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class); return Optional.ofNullable(texturePayload.textures); } else { return Optional.empty(); @@ -148,7 +179,12 @@ public class YggdrasilService { if (!clientToken.equals(response.clientToken)) throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken); - return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user); + return new YggdrasilSession( + response.clientToken, + response.accessToken, + response.selectedProfile, + response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles), + response.user); } private static void requireEmpty(String response) throws AuthenticationException { @@ -168,7 +204,7 @@ public class YggdrasilService { } } - private String request(URL url, Object payload) throws AuthenticationException { + private static String request(URL url, Object payload) throws AuthenticationException { try { if (payload == null) return NetworkUtils.doGet(url); @@ -187,26 +223,25 @@ public class YggdrasilService { } } - private class TextureResponse { + private static class TextureResponse { public Map textures; } - private class AuthenticationResponse extends ErrorResponse { + private static class AuthenticationResponse extends ErrorResponse { public String accessToken; public String clientToken; public GameProfile selectedProfile; - public GameProfile[] availableProfiles; + public List availableProfiles; public User user; } - private class ErrorResponse { + private static class ErrorResponse { public String error; public String errorMessage; public String cause; } private static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE) .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) .create(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java index 7785c06ed..c87a47b78 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java @@ -18,10 +18,11 @@ package org.jackhuang.hmcl.auth.yggdrasil; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -30,15 +31,16 @@ import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Pair.pair; +@Immutable public class YggdrasilSession { - private String clientToken; - private String accessToken; - private GameProfile selectedProfile; - private GameProfile[] availableProfiles; - private User user; + private final String clientToken; + private final String accessToken; + private final GameProfile selectedProfile; + private final List availableProfiles; + private final User user; - public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) { + public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List availableProfiles, User user) { this.clientToken = clientToken; this.accessToken = accessToken; this.selectedProfile = selectedProfile; @@ -64,7 +66,7 @@ public class YggdrasilSession { /** * @return nullable (null if the YggdrasilSession is loaded from storage) */ - public GameProfile[] getAvailableProfiles() { + public List getAvailableProfiles() { return availableProfiles; } @@ -78,7 +80,7 @@ public class YggdrasilSession { String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing")); String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing")); String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing")); - PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null); + Map userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null); return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties)); } @@ -107,5 +109,5 @@ public class YggdrasilSession { Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}")); } - private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create(); + private static final Gson GSON_PROPERTIES = new Gson(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/InvocationDispatcher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/InvocationDispatcher.java index 274e05edb..c9710e9ba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/InvocationDispatcher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/InvocationDispatcher.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.util; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Supplier; @@ -31,8 +32,8 @@ import java.util.function.Supplier; */ public class InvocationDispatcher implements Consumer { - public static InvocationDispatcher runOn(Consumer executor, Consumer action) { - return new InvocationDispatcher<>(arg -> executor.accept(() -> { + public static InvocationDispatcher runOn(Executor executor, Consumer action) { + return new InvocationDispatcher<>(arg -> executor.execute(() -> { synchronized (action) { action.accept(arg.get()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 1505ce1dc..6d5ee2b82 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -18,7 +18,10 @@ package org.jackhuang.hmcl.util; import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.jackhuang.hmcl.util.function.ExceptionalRunnable; import org.jackhuang.hmcl.util.function.ExceptionalSupplier; @@ -172,6 +175,17 @@ public final class Lang { return thread; } + public static ThreadPoolExecutor threadPool(String name, boolean daemon, int threads, long timeout, TimeUnit timeunit) { + AtomicInteger counter = new AtomicInteger(1); + ThreadPoolExecutor pool = new ThreadPoolExecutor(0, threads, timeout, timeunit, new LinkedBlockingQueue<>(), r -> { + Thread t = new Thread(r, name + "-" + counter.getAndIncrement()); + t.setDaemon(daemon); + return t; + }); + pool.allowsCoreThreadTimeOut(); + return pool; + } + public static int parseInt(Object string, int defaultValue) { try { return Integer.parseInt(string.toString()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/UUIDTypeAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/UUIDTypeAdapter.java index 0f4b24929..920162250 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/UUIDTypeAdapter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/UUIDTypeAdapter.java @@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter { public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter(); - private UUIDTypeAdapter() { - } - @Override public void write(JsonWriter writer, UUID value) throws IOException { writer.value(value == null ? null : fromUUID(value)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java index 34e586b23..7f6ce0cb0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java @@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx; import static java.util.Objects.requireNonNull; +import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Function; import java.util.function.Supplier; +import org.jackhuang.hmcl.util.InvocationDispatcher; + +import javafx.application.Platform; import javafx.beans.binding.ObjectBinding; import javafx.beans.value.ObservableValue; @@ -53,6 +58,10 @@ public abstract class MultiStepBinding extends ObjectBinding { return new FlatMappedBinding<>(map(mapper), nullAlternative); } + public MultiStepBinding asyncMap(Function mapper, V initial, Executor executor) { + return new AsyncMappedBinding<>(this, mapper, executor, initial); + } + private static class SimpleBinding extends MultiStepBinding { public SimpleBinding(ObservableValue predecessor) { @@ -68,6 +77,11 @@ public abstract class MultiStepBinding extends ObjectBinding { public MultiStepBinding map(Function mapper) { return new MappedBinding<>(predecessor, mapper); } + + @Override + public MultiStepBinding asyncMap(Function mapper, V initial, Executor executor) { + return new AsyncMappedBinding<>(predecessor, mapper, executor, initial); + } } private static class MappedBinding extends MultiStepBinding { @@ -119,4 +133,52 @@ public abstract class MultiStepBinding extends ObjectBinding { } } } + + private static class AsyncMappedBinding extends MultiStepBinding { + + private final InvocationDispatcher dispatcher; + + private boolean initialized = false; + private T prev; + private U value; + + public AsyncMappedBinding(ObservableValue predecessor, Function 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; + } + + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableCache.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableCache.java new file mode 100644 index 000000000..937d6ca87 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableCache.java @@ -0,0 +1,162 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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 { + + private final ExceptionalFunction source; + private final BiConsumer exceptionHandler; + private final V fallbackValue; + private final Executor executor; + private final ObservableHelper observable = new ObservableHelper(); + private final Map cache = new HashMap<>(); + private final Map> pendings = new HashMap<>(); + private final Map invalidated = new HashMap<>(); + + public ObservableCache(ExceptionalFunction source, BiConsumer exceptionHandler, V fallbackValue, Executor executor) { + this.source = source; + this.exceptionHandler = exceptionHandler; + this.fallbackValue = fallbackValue; + this.executor = executor; + } + + public Optional 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 query(K key, Executor executor) { + CompletableFuture future; + synchronized (this) { + CompletableFuture 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 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); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java index 171e54649..5e5128957 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableHelper.java @@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener { private List listeners = new CopyOnWriteArrayList<>(); private Observable source; + public ObservableHelper() { + this.source = this; + } + public ObservableHelper(Observable source) { this.source = source; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableOptionalCache.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableOptionalCache.java new file mode 100644 index 000000000..70ccc2a79 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/ObservableOptionalCache.java @@ -0,0 +1,62 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui 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 . + */ +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 { + + private final ObservableCache, E> backed; + + public ObservableOptionalCache(ExceptionalFunction, E> source, BiConsumer exceptionHandler, Executor executor) { + backed = new ObservableCache<>(source, exceptionHandler, Optional.empty(), executor); + } + + public Optional 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 get(K key) { + return backed.get(key); + } + + public Optional getDirectly(K key) throws E { + return backed.getDirectly(key); + } + + public ObjectBinding> binding(K key) { + return backed.binding(key); + } + + public void invalidate(K key) { + backed.invalidate(key); + } +}