Merge branch 'skin-refactor' into javafx
This commit is contained in:
@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
|
||||
|
||||
public abstract Map<Object, Object> toStorage();
|
||||
|
||||
public abstract void clearCache();
|
||||
public void clearCache() {
|
||||
}
|
||||
|
||||
private ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
|
||||
@@ -17,27 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Select character by name.
|
||||
* Thrown when a previously existing character cannot be found.
|
||||
*/
|
||||
public class SpecificCharacterSelector implements CharacterSelector {
|
||||
private UUID uuid;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param uuid character's uuid.
|
||||
*/
|
||||
public SpecificCharacterSelector(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
public final class CharacterDeletedException extends AuthenticationException {
|
||||
public CharacterDeletedException() {
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,7 +34,6 @@ public interface CharacterSelector {
|
||||
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
|
||||
* @return your choice of game profile.
|
||||
*/
|
||||
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
GameProfile select(YggdrasilService yggdrasilService, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
|
||||
CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
}
|
||||
|
||||
@@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* (A account may hold more than one characters.)
|
||||
*/
|
||||
public final class NoCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
public NoCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoCharacterException() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NoSelectedCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param account the error yggdrasil account.
|
||||
*/
|
||||
public NoSelectedCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoSelectedCharacterException() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private AuthlibInjectorServer server;
|
||||
private final AuthlibInjectorServer server;
|
||||
private AuthlibInjectorArtifactProvider downloader;
|
||||
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
super(service, username, characterUUID, session);
|
||||
|
||||
this.downloader = downloader;
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
super(server.getYggdrasilService(), username, password, selector);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) {
|
||||
super(server.getYggdrasilService(), username, session);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password, selector));
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
super.clearCache();
|
||||
server.invalidateMetadataCache();
|
||||
}
|
||||
|
||||
public AuthlibInjectorServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
||||
|
||||
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
||||
|
||||
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, session.getSelectedProfile().getId(), session);
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
return new AuthlibInjectorAccount(server, downloader, username, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String> links = emptyMap();
|
||||
|
||||
private transient boolean metadataRefreshed;
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient YggdrasilService yggdrasilService;
|
||||
|
||||
public AuthlibInjectorServer(String url) {
|
||||
this.url = url;
|
||||
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return yggdrasilService;
|
||||
}
|
||||
|
||||
public Optional<String> getMetadataResponse() {
|
||||
return Optional.ofNullable(metadataResponse);
|
||||
}
|
||||
@@ -222,6 +229,10 @@ public class AuthlibInjectorServer implements Observable {
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateMetadataCache() {
|
||||
metadataRefreshed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
|
||||
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
|
||||
OfflineAccount(String username, UUID uuid) {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(uuid);
|
||||
protected OfflineAccount(String username, UUID uuid) {
|
||||
this.username = requireNonNull(username);
|
||||
this.uuid = requireNonNull(uuid);
|
||||
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
|
||||
if (StringUtils.isBlank(username))
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(username))
|
||||
throw new AuthenticationException("Username cannot be empty");
|
||||
|
||||
public AuthInfo logIn() {
|
||||
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
return Optional.empty();
|
||||
return Optional.of(logIn());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
// Nothing to clear.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
@Immutable
|
||||
public class CompleteGameProfile extends GameProfile {
|
||||
|
||||
@JsonAdapter(PropertyMapSerializer.class)
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
|
||||
super(id, name);
|
||||
this.properties = requireNonNull(properties);
|
||||
}
|
||||
|
||||
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
|
||||
this(profile.getId(), profile.getName(), properties);
|
||||
}
|
||||
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
super.validate();
|
||||
|
||||
if (properties == null)
|
||||
throw new JsonParseException("Game profile properties cannot be null");
|
||||
}
|
||||
}
|
||||
@@ -17,36 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class GameProfile implements Validation {
|
||||
public class GameProfile implements Validation {
|
||||
|
||||
@JsonAdapter(UUIDTypeAdapter.class)
|
||||
private final UUID id;
|
||||
private final String name;
|
||||
private final PropertyMap properties;
|
||||
|
||||
public GameProfile() {
|
||||
this(null, null);
|
||||
}
|
||||
private final String name;
|
||||
|
||||
public GameProfile(UUID id, String name) {
|
||||
this(id, name, new PropertyMap());
|
||||
}
|
||||
|
||||
public GameProfile(UUID id, String name, PropertyMap properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.properties = properties;
|
||||
this.id = requireNonNull(id);
|
||||
this.name = requireNonNull(name);
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable
|
||||
*/
|
||||
public PropertyMap getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (id == null)
|
||||
throw new JsonParseException("Game profile id cannot be null or malformed");
|
||||
if (StringUtils.isBlank(name))
|
||||
throw new JsonParseException("Game profile name cannot be null or blank");
|
||||
throw new JsonParseException("Game profile id cannot be null");
|
||||
if (name == null)
|
||||
throw new JsonParseException("Game profile name cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
|
||||
|
||||
@Override
|
||||
public URL getAuthenticationURL() {
|
||||
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "mojang";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PropertyMap extends HashMap<String, String> {
|
||||
|
||||
public static PropertyMap fromMap(Map<?, ?> map) {
|
||||
PropertyMap propertyMap = new PropertyMap();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
|
||||
propertyMap.put((String) entry.getKey(), (String) entry.getValue());
|
||||
}
|
||||
return propertyMap;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
PropertyMap result = new PropertyMap();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Map.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", entry.getKey());
|
||||
object.addProperty("value", entry.getValue());
|
||||
result.add(object);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LegacySerializer
|
||||
implements JsonSerializer<PropertyMap> {
|
||||
public static final LegacySerializer INSTANCE = new LegacySerializer();
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject result = new JsonObject();
|
||||
for (PropertyMap.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonArray values = new JsonArray();
|
||||
values.add(new JsonPrimitive(entry.getValue()));
|
||||
result.add(entry.getKey(), values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
|
||||
|
||||
@Override
|
||||
public Map<String, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return unmodifiableMap(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
src.forEach((k, v) -> {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", k);
|
||||
object.addProperty("value", v);
|
||||
result.add(object);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,7 @@ public final class Texture {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getMetadata(String key) {
|
||||
if (metadata == null)
|
||||
return null;
|
||||
else
|
||||
return metadata.get(key);
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public enum TextureModel {
|
||||
STEVE("default"), ALEX("slim");
|
||||
|
||||
public final String modelName;
|
||||
|
||||
private TextureModel(String modelName) {
|
||||
this.modelName = modelName;
|
||||
}
|
||||
|
||||
public static TextureModel detectModelName(Map<String, String> metadata) {
|
||||
if (metadata != null && "slim".equals(metadata.get("model"))) {
|
||||
return ALEX;
|
||||
} else {
|
||||
return STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
public static TextureModel detectUUID(UUID uuid) {
|
||||
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,31 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
@Immutable
|
||||
public final class User implements Validation {
|
||||
|
||||
private final String id;
|
||||
private final PropertyMap properties;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public User(String id) {
|
||||
this(id, null);
|
||||
}
|
||||
|
||||
public User(String id, PropertyMap properties) {
|
||||
public User(String id, Map<String, String> properties) {
|
||||
this.id = id;
|
||||
this.properties = properties;
|
||||
}
|
||||
@@ -43,7 +51,7 @@ public final class User implements Validation {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -52,5 +60,4 @@ public final class User implements Validation {
|
||||
if (StringUtils.isBlank(id))
|
||||
throw new JsonParseException("User id cannot be empty.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,32 +17,62 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterDeletedException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class YggdrasilAccount extends Account {
|
||||
|
||||
private final String username;
|
||||
private final YggdrasilService service;
|
||||
private boolean isOnline = false;
|
||||
private final UUID characterUUID;
|
||||
private final String username;
|
||||
|
||||
private boolean authenticated = false;
|
||||
private YggdrasilSession session;
|
||||
private UUID characterUUID;
|
||||
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
this.service = service;
|
||||
this.username = username;
|
||||
this.session = session;
|
||||
this.characterUUID = characterUUID;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
|
||||
this.session = requireNonNull(session);
|
||||
}
|
||||
|
||||
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
|
||||
this.session = null;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new NoCharacterException();
|
||||
}
|
||||
|
||||
GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles());
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
characterToSelect);
|
||||
// response validity has been checked in refresh()
|
||||
} else {
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
characterUUID = session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
|
||||
return session.getSelectedProfile().getName();
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return session != null && StringUtils.isNotBlank(session.getAccessToken());
|
||||
}
|
||||
|
||||
public boolean canPlayOnline() {
|
||||
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||
if (!canPlayOnline()) {
|
||||
if (!authenticated) {
|
||||
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
isOnline = true;
|
||||
authenticated = true;
|
||||
} else {
|
||||
YggdrasilSession acquiredSession;
|
||||
try {
|
||||
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
|
||||
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
} catch (RemoteAuthenticationException e) {
|
||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||
throw new CredentialExpiredException(e);
|
||||
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (acquiredSession.getSelectedProfile() == null ||
|
||||
!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new ServerResponseMalformedException("Selected profile changed");
|
||||
}
|
||||
|
||||
session = acquiredSession;
|
||||
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
|
||||
}
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
|
||||
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
|
||||
* has a profile selected.
|
||||
*
|
||||
* @param acquiredSession the session acquired by making an authenticate/refresh request
|
||||
*/
|
||||
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
|
||||
throw new NoCharacterException(this);
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
|
||||
this.session = service.refresh(
|
||||
GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream()
|
||||
.filter(charatcer -> charatcer.getId().equals(characterUUID))
|
||||
.findFirst()
|
||||
.orElseThrow(CharacterDeletedException::new);
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
|
||||
characterToSelect);
|
||||
|
||||
} else {
|
||||
this.session = acquiredSession;
|
||||
if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
this.characterUUID = this.session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
|
||||
return Optional.of(session.toAuthInfo());
|
||||
|
||||
return Optional.empty();
|
||||
return Optional.of(session.toAuthInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toStorage() {
|
||||
if (session == null)
|
||||
throw new IllegalStateException("No session is specified");
|
||||
|
||||
HashMap<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", getUsername());
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", username);
|
||||
storage.putAll(session.toStorage());
|
||||
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
|
||||
storage.put("profileProperties", profile.getProperties());
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
if (session == null || session.getSelectedProfile() == null)
|
||||
return null;
|
||||
else
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin() throws AuthenticationException {
|
||||
return getSkin(session.getSelectedProfile());
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
|
||||
if (!service.getTextures(profile).isPresent()) {
|
||||
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
|
||||
}
|
||||
|
||||
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
Optional.ofNullable(session)
|
||||
.map(YggdrasilSession::getSelectedProfile)
|
||||
.map(GameProfile::getProperties)
|
||||
.ifPresent(it -> it.remove("textures"));
|
||||
authenticated = false;
|
||||
service.getProfileRepository().invalidate(characterUUID);
|
||||
}
|
||||
|
||||
private static String randomClientToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "YggdrasilAccount[username=" + getUsername() + "]";
|
||||
return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
@@ -34,10 +32,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
*/
|
||||
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
private YggdrasilService service;
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(password);
|
||||
|
||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new YggdrasilAccount(service, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
||||
|
||||
return new YggdrasilAccount(new YggdrasilService(provider), username, session.getSelectedProfile().getId(), session);
|
||||
}
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
public static String randomToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
return new YggdrasilAccount(service, username, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
|
||||
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public YggdrasilService(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
this.profileRepository = new ObservableOptionalCache<>(
|
||||
uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid + " from " + provider);
|
||||
return getCompleteGameProfile(uuid);
|
||||
},
|
||||
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid + " from " + provider, e),
|
||||
POOL);
|
||||
}
|
||||
|
||||
public ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> getProfileRepository() {
|
||||
return profileRepository;
|
||||
}
|
||||
|
||||
public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException {
|
||||
@@ -62,7 +85,7 @@ public class YggdrasilService {
|
||||
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
|
||||
}
|
||||
|
||||
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
private static Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("accessToken", accessToken);
|
||||
request.put("clientToken", clientToken);
|
||||
@@ -82,7 +105,16 @@ public class YggdrasilService {
|
||||
pair("name", characterToSelect.getName())));
|
||||
}
|
||||
|
||||
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
|
||||
if (characterToSelect != null) {
|
||||
if (response.getSelectedProfile() == null ||
|
||||
!response.getSelectedProfile().getId().equals(characterToSelect.getId())) {
|
||||
throw new ServerResponseMalformedException("Failed to select character");
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public boolean validate(String accessToken) throws AuthenticationException {
|
||||
@@ -121,20 +153,19 @@ public class YggdrasilService {
|
||||
* @param uuid the uuid that the character corresponding to.
|
||||
* @return the complete game profile(filled with more properties)
|
||||
*/
|
||||
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
Objects.requireNonNull(uuid);
|
||||
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), CompleteGameProfile.class));
|
||||
}
|
||||
|
||||
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
|
||||
public static Optional<Map<TextureType, Texture>> getTextures(CompleteGameProfile profile) throws ServerResponseMalformedException {
|
||||
Objects.requireNonNull(profile);
|
||||
|
||||
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
|
||||
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
|
||||
String encodedTextures = profile.getProperties().get("textures");
|
||||
|
||||
if (encodedTextures.isPresent()) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
|
||||
if (encodedTextures != null) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
|
||||
return Optional.ofNullable(texturePayload.textures);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
@@ -148,7 +179,12 @@ public class YggdrasilService {
|
||||
if (!clientToken.equals(response.clientToken))
|
||||
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
|
||||
|
||||
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
|
||||
return new YggdrasilSession(
|
||||
response.clientToken,
|
||||
response.accessToken,
|
||||
response.selectedProfile,
|
||||
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
|
||||
response.user);
|
||||
}
|
||||
|
||||
private static void requireEmpty(String response) throws AuthenticationException {
|
||||
@@ -168,7 +204,7 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private String request(URL url, Object payload) throws AuthenticationException {
|
||||
private static String request(URL url, Object payload) throws AuthenticationException {
|
||||
try {
|
||||
if (payload == null)
|
||||
return NetworkUtils.doGet(url);
|
||||
@@ -187,26 +223,25 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private class TextureResponse {
|
||||
private static class TextureResponse {
|
||||
public Map<TextureType, Texture> textures;
|
||||
}
|
||||
|
||||
private class AuthenticationResponse extends ErrorResponse {
|
||||
private static class AuthenticationResponse extends ErrorResponse {
|
||||
public String accessToken;
|
||||
public String clientToken;
|
||||
public GameProfile selectedProfile;
|
||||
public GameProfile[] availableProfiles;
|
||||
public List<GameProfile> availableProfiles;
|
||||
public User user;
|
||||
}
|
||||
|
||||
private class ErrorResponse {
|
||||
private static class ErrorResponse {
|
||||
public String error;
|
||||
public String errorMessage;
|
||||
public String cause;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
.create();
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -30,15 +31,16 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@Immutable
|
||||
public class YggdrasilSession {
|
||||
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
private GameProfile selectedProfile;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
private final String clientToken;
|
||||
private final String accessToken;
|
||||
private final GameProfile selectedProfile;
|
||||
private final List<GameProfile> availableProfiles;
|
||||
private final User user;
|
||||
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
|
||||
this.clientToken = clientToken;
|
||||
this.accessToken = accessToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
@@ -64,7 +66,7 @@ public class YggdrasilSession {
|
||||
/**
|
||||
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
||||
*/
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
public List<GameProfile> getAvailableProfiles() {
|
||||
return availableProfiles;
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ public class YggdrasilSession {
|
||||
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
|
||||
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
|
||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
|
||||
}
|
||||
|
||||
@@ -107,5 +109,5 @@ public class YggdrasilSession {
|
||||
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
|
||||
}
|
||||
|
||||
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
|
||||
private static final Gson GSON_PROPERTIES = new Gson();
|
||||
}
|
||||
|
||||
@@ -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<ARG> implements Consumer<ARG> {
|
||||
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.accept(() -> {
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.execute(() -> {
|
||||
synchronized (action) {
|
||||
action.accept(arg.get());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
||||
|
||||
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
|
||||
|
||||
private UUIDTypeAdapter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter writer, UUID value) throws IOException {
|
||||
writer.value(value == null ? null : fromUUID(value));
|
||||
|
||||
@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
return new FlatMappedBinding<>(map(mapper), nullAlternative);
|
||||
}
|
||||
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<U, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(this, mapper, executor, initial);
|
||||
}
|
||||
|
||||
private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
|
||||
|
||||
public SimpleBinding(ObservableValue<T> predecessor) {
|
||||
@@ -68,6 +77,11 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
|
||||
return new MappedBinding<>(predecessor, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<T, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(predecessor, mapper, executor, initial);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
@@ -119,4 +133,52 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AsyncMappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
|
||||
private final InvocationDispatcher<T> dispatcher;
|
||||
|
||||
private boolean initialized = false;
|
||||
private T prev;
|
||||
private U value;
|
||||
|
||||
public AsyncMappedBinding(ObservableValue<T> predecessor, Function<T, U> mapper, Executor executor, U initial) {
|
||||
super(predecessor);
|
||||
this.value = initial;
|
||||
|
||||
dispatcher = InvocationDispatcher.runOn(executor, arg -> {
|
||||
synchronized (this) {
|
||||
if (initialized && Objects.equals(arg, prev)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
U newValue = mapper.apply(arg);
|
||||
synchronized (this) {
|
||||
prev = arg;
|
||||
value = newValue;
|
||||
initialized = true;
|
||||
}
|
||||
Platform.runLater(this::invalidate);
|
||||
});
|
||||
}
|
||||
|
||||
// called on FX thread, this method is serial
|
||||
@Override
|
||||
protected U computeValue() {
|
||||
T currentPrev = predecessor.getValue();
|
||||
U value;
|
||||
boolean updateNeeded = false;
|
||||
synchronized (this) {
|
||||
value = this.value;
|
||||
if (!initialized || !Objects.equals(currentPrev, prev)) {
|
||||
updateNeeded = true;
|
||||
}
|
||||
}
|
||||
if (updateNeeded) {
|
||||
dispatcher.accept(currentPrev);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableCache<K, V, E extends Exception> {
|
||||
|
||||
private final ExceptionalFunction<K, V, E> source;
|
||||
private final BiConsumer<K, Throwable> exceptionHandler;
|
||||
private final V fallbackValue;
|
||||
private final Executor executor;
|
||||
private final ObservableHelper observable = new ObservableHelper();
|
||||
private final Map<K, V> cache = new HashMap<>();
|
||||
private final Map<K, CompletableFuture<V>> pendings = new HashMap<>();
|
||||
private final Map<K, Boolean> invalidated = new HashMap<>();
|
||||
|
||||
public ObservableCache(ExceptionalFunction<K, V, E> source, BiConsumer<K, Throwable> exceptionHandler, V fallbackValue, Executor executor) {
|
||||
this.source = source;
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
this.fallbackValue = fallbackValue;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
synchronized (this) {
|
||||
return Optional.ofNullable(cache.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
synchronized (this) {
|
||||
cache.put(key, value);
|
||||
invalidated.remove(key);
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
|
||||
private CompletableFuture<V> query(K key, Executor executor) {
|
||||
CompletableFuture<V> future;
|
||||
synchronized (this) {
|
||||
CompletableFuture<V> prev = pendings.get(key);
|
||||
if (prev != null) {
|
||||
return prev;
|
||||
} else {
|
||||
future = new CompletableFuture<>();
|
||||
pendings.put(key, future);
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
V result;
|
||||
try {
|
||||
result = source.apply(key);
|
||||
} catch (Throwable ex) {
|
||||
synchronized (this) {
|
||||
pendings.remove(key);
|
||||
}
|
||||
exceptionHandler.accept(key, ex);
|
||||
future.completeExceptionally(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
cache.put(key, result);
|
||||
invalidated.remove(key);
|
||||
pendings.remove(key, future);
|
||||
}
|
||||
future.complete(result);
|
||||
Platform.runLater(observable::invalidate);
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V cached;
|
||||
synchronized (this) {
|
||||
cached = cache.get(key);
|
||||
if (cached != null && !invalidated.containsKey(key)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return query(key, Runnable::run).join();
|
||||
} catch (CompletionException | CancellationException ignored) {
|
||||
}
|
||||
|
||||
if (cached == null) {
|
||||
return fallbackValue;
|
||||
} else {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
public V getDirectly(K key) throws E {
|
||||
V result = source.apply(key);
|
||||
put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public ObjectBinding<V> binding(K key) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
V result;
|
||||
boolean refresh;
|
||||
synchronized (this) {
|
||||
result = cache.get(key);
|
||||
if (result == null) {
|
||||
result = fallbackValue;
|
||||
refresh = true;
|
||||
} else {
|
||||
refresh = invalidated.containsKey(key);
|
||||
}
|
||||
}
|
||||
if (refresh) {
|
||||
query(key, executor);
|
||||
}
|
||||
return result;
|
||||
}, observable);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
synchronized (this) {
|
||||
if (cache.containsKey(key)) {
|
||||
invalidated.put(key, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener {
|
||||
private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private Observable source;
|
||||
|
||||
public ObservableHelper() {
|
||||
this.source = this;
|
||||
}
|
||||
|
||||
public ObservableHelper(Observable source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableOptionalCache<K, V, E extends Exception> {
|
||||
|
||||
private final ObservableCache<K, Optional<V>, E> backed;
|
||||
|
||||
public ObservableOptionalCache(ExceptionalFunction<K, Optional<V>, E> source, BiConsumer<K, Throwable> exceptionHandler, Executor executor) {
|
||||
backed = new ObservableCache<>(source, exceptionHandler, Optional.empty(), executor);
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
return backed.getImmediately(key).flatMap(it -> it);
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
backed.put(key, Optional.of(value));
|
||||
}
|
||||
|
||||
public Optional<V> get(K key) {
|
||||
return backed.get(key);
|
||||
}
|
||||
|
||||
public Optional<V> getDirectly(K key) throws E {
|
||||
return backed.getDirectly(key);
|
||||
}
|
||||
|
||||
public ObjectBinding<Optional<V>> binding(K key) {
|
||||
return backed.binding(key);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
backed.invalidate(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user