feat: support change skin of offline accounts.

This commit is contained in:
huanghongxun
2021-09-25 02:04:25 +08:00
parent 01893b053d
commit cd030c1de0
24 changed files with 672 additions and 192 deletions

View File

@@ -17,13 +17,14 @@
*/
package org.jackhuang.hmcl.auth.offline;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.StringUtils;
@@ -51,13 +52,13 @@ public class OfflineAccount extends Account {
private final AuthlibInjectorArtifactProvider downloader;
private final String username;
private final UUID uuid;
private final Map<TextureType, Texture> textures;
private Skin skin;
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Map<TextureType, Texture> textures) {
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Skin skin) {
this.downloader = requireNonNull(downloader);
this.username = requireNonNull(username);
this.uuid = requireNonNull(uuid);
this.textures = textures;
this.skin = skin;
if (StringUtils.isBlank(username)) {
throw new IllegalArgumentException("Username cannot be blank");
@@ -79,11 +80,20 @@ public class OfflineAccount extends Account {
return username;
}
public Skin getSkin() {
return skin;
}
public void setSkin(Skin skin) {
this.skin = skin;
invalidate();
}
@Override
public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
if (skin != null || cape != null) {
if (skin != null) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return downloader.getArtifactInfo();
@@ -109,18 +119,14 @@ public class OfflineAccount extends Account {
try {
YggdrasilServer server = new YggdrasilServer(0);
server.start();
server.addCharacter(new YggdrasilServer.Character(uuid, username, TextureModel.STEVE,
mapOf(
pair(TextureType.SKIN, server.loadTexture(skin)),
pair(TextureType.CAPE, server.loadTexture(cape))
)));
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
return authInfo.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
"-Dauthlibinjector.side=client"
))
.withCloseable(server::stop);
} catch (IOException e) {
} catch (Exception e) {
throw new AuthenticationException(e);
}
} else {
@@ -143,18 +149,20 @@ public class OfflineAccount extends Account {
return mapOf(
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
pair("username", username),
pair("skin", skin),
pair("cape", cape)
pair("skin", skin.toStorage())
);
}
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return super.getTextures();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("username", username)
.append("uuid", uuid)
.append("skin", skin)
.append("cape", cape)
.toString();
}

View File

@@ -45,25 +45,23 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
}
public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(downloader, username, uuid, null, null);
return new OfflineAccount(downloader, username, uuid, null);
}
@Override
public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) {
AdditionalData data;
UUID uuid;
String skin;
String cape;
Skin skin;
if (additionalData != null) {
data = (AdditionalData) additionalData;
uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid;
skin = data.skin;
cape = data.cape;
} else {
uuid = getUUIDFromUserName(username);
skin = cape = null;
skin = null;
}
return new OfflineAccount(downloader, username, uuid, skin, cape);
return new OfflineAccount(downloader, username, uuid, skin);
}
@Override
@@ -73,10 +71,9 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
UUID uuid = tryCast(storage.get("uuid"), String.class)
.map(UUIDTypeAdapter::fromString)
.orElse(getUUIDFromUserName(username));
String skin = tryCast(storage.get("skin"), String.class).orElse(null);
String cape = tryCast(storage.get("cape"), String.class).orElse(null);
Skin skin = Skin.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null));
return new OfflineAccount(downloader, username, uuid, skin, cape);
return new OfflineAccount(downloader, username, uuid, skin);
}
public static UUID getUUIDFromUserName(String username) {
@@ -85,13 +82,11 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
public static class AdditionalData {
private final UUID uuid;
private final String skin;
private final String cape;
private final Skin skin;
public AdditionalData(UUID uuid, String skin, String cape) {
public AdditionalData(UUID uuid, Skin skin) {
this.uuid = uuid;
this.skin = skin;
this.cape = cape;
}
}

View File

@@ -18,26 +18,31 @@
package org.jackhuang.hmcl.auth.offline;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Skin {
@@ -46,33 +51,81 @@ public class Skin {
STEVE,
ALEX,
LOCAL_FILE,
LITTLE_SKIN,
CUSTOM_SKIN_LOADER_API,
YGGDRASIL_API
YGGDRASIL_API;
public static Type fromStorage(String type) {
switch (type) {
case "default":
return DEFAULT;
case "steve":
return STEVE;
case "alex":
return ALEX;
case "local_file":
return LOCAL_FILE;
case "little_skin":
return LITTLE_SKIN;
case "custom_skin_loader_api":
return CUSTOM_SKIN_LOADER_API;
case "yggdrasil_api":
return YGGDRASIL_API;
default:
return null;
}
}
}
private Type type;
private String value;
private final Type type;
private final String cslApi;
private final String localSkinPath;
private final String localCapePath;
public Skin(Type type, String cslApi, String localSkinPath, String localCapePath) {
this.type = type;
this.cslApi = cslApi;
this.localSkinPath = localSkinPath;
this.localCapePath = localCapePath;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
public String getCslApi() {
return cslApi;
}
public Task<Texture> toTexture(String username) {
public String getLocalSkinPath() {
return localSkinPath;
}
public String getLocalCapePath() {
return localCapePath;
}
public Task<LoadedSkin> load(String username) {
switch (type) {
case DEFAULT:
return Task.supplyAsync(() -> null);
case STEVE:
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")));
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
case ALEX:
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")));
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null));
case LOCAL_FILE:
return Task.supplyAsync(() -> Texture.loadTexture(Files.newInputStream(Paths.get(value))));
return Task.supplyAsync(() -> {
Texture skin = null, cape = null;
Optional<Path> skinPath = FileUtils.tryGetPath(localSkinPath);
Optional<Path> capePath = FileUtils.tryGetPath(localCapePath);
if (skinPath.isPresent()) skin = Texture.loadTexture(Files.newInputStream(skinPath.get()));
if (capePath.isPresent()) cape = Texture.loadTexture(Files.newInputStream(capePath.get()));
return new LoadedSkin(TextureModel.STEVE, skin, cape);
});
case LITTLE_SKIN:
case CUSTOM_SKIN_LOADER_API:
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", value, username))))
String realCslApi = type == Type.LITTLE_SKIN ? "http://mcskin.littleservice.cn" : cslApi;
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", realCslApi, username))))
.thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@@ -80,13 +133,57 @@ public class Skin {
return Task.supplyAsync(() -> null);
}
return new FetchBytesTask(new URL(String.format("%s/textures/%s", value, result.getHash())), 3);
}).thenApplyAsync(Texture::loadTexture);
return Task.allOf(
Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
);
}).thenApplyAsync(result -> {
if (result == null) {
return null;
}
Texture skin, cape;
if (result.get(1) != null) {
skin = Texture.loadTexture((InputStream) result.get(1));
} else {
skin = null;
}
if (result.get(2) != null) {
cape = Texture.loadTexture((InputStream) result.get(2));
} else {
cape = null;
}
return new LoadedSkin((TextureModel) result.get(0), skin, cape);
});
default:
throw new UnsupportedOperationException();
}
}
public Map<?, ?> toStorage() {
return mapOf(
pair("type", type.name().toLowerCase(Locale.ROOT)),
pair("cslApi", cslApi),
pair("localSkinPath", localSkinPath),
pair("localCapePath", localCapePath)
);
}
public static Skin fromStorage(Map<?, ?> storage) {
if (storage == null) return null;
Type type = tryCast(storage.get("type"), String.class).flatMap(t -> Optional.ofNullable(Type.fromStorage(t)))
.orElse(Type.DEFAULT);
String cslApi = tryCast(storage.get("cslApi"), String.class).orElse(null);
String localSkinPath = tryCast(storage.get("localSkinPath"), String.class).orElse(null);
String localCapePath = tryCast(storage.get("localCapePath"), String.class).orElse(null);
return new Skin(type, cslApi, localSkinPath, localCapePath);
}
private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) {
@@ -127,6 +224,30 @@ public class Skin {
}
}
public static class LoadedSkin {
private final TextureModel model;
private final Texture skin;
private final Texture cape;
public LoadedSkin(TextureModel model, Texture skin, Texture cape) {
this.model = model;
this.skin = skin;
this.cape = cape;
}
public TextureModel getModel() {
return model;
}
public Texture getSkin() {
return skin;
}
public Texture getCape() {
return cape;
}
}
private static class SkinJson {
private final String username;
private final String skin;
@@ -183,6 +304,12 @@ public class Skin {
return null;
}
public String getCapeHash() {
if (textures != null && textures.cape != null) {
return textures.cape;
} else return cape;
}
public static class TextureJson {
@SerializedName("default")
private final String defaultSkin;

View File

@@ -42,6 +42,10 @@ public class Texture {
this.data = requireNonNull(data);
}
public byte[] getData() {
return data;
}
public String getHash() {
return hash;
}

View File

@@ -19,8 +19,6 @@ package org.jackhuang.hmcl.auth.offline;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.KeyUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -36,7 +34,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -143,14 +140,12 @@ public class YggdrasilServer extends HttpServer {
public static class Character {
private final UUID uuid;
private final String name;
private final TextureModel model;
private final Map<TextureType, Texture> textures;
private final Skin.LoadedSkin skin;
public Character(UUID uuid, String name, TextureModel model, Map<TextureType, Texture> textures) {
public Character(UUID uuid, String name, Skin.LoadedSkin skin) {
this.uuid = uuid;
this.name = name;
this.model = model;
this.textures = textures;
this.skin = Objects.requireNonNull(skin);
}
public UUID getUUID() {
@@ -161,30 +156,17 @@ public class YggdrasilServer extends HttpServer {
return name;
}
public TextureModel getModel() {
return model;
}
public Map<TextureType, Texture> getTextures() {
return textures;
}
private Map<String, Object> createKeyValue(String key, String value) {
return mapOf(
pair("name", key),
pair("value", value)
);
}
public GameProfile toSimpleResponse() {
return new GameProfile(uuid, name);
}
public Object toCompleteResponse(String rootUrl) {
Map<String, Object> realTextures = new HashMap<>();
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) {
if (textureEntry.getValue() == null) continue;
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().getHash())));
if (skin.getSkin() != null) {
realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
}
if (skin.getCape() != null) {
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
}
Map<String, Object> textureResponse = mapOf(

View File

@@ -340,7 +340,7 @@ public abstract class Task<T> {
messageUpdate.accept(newMessage);
}
public final void run() throws Exception {
public final T run() throws Exception {
if (getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + getName());
@@ -350,6 +350,8 @@ public abstract class Task<T> {
for (Task<?> task : getDependencies())
doSubTask(task);
onDone.fireEvent(new TaskEvent(this, this, false));
return getResult();
}
private void doSubTask(Task<?> task) throws Exception {

View File

@@ -413,6 +413,7 @@ public final class FileUtils {
}
public static Optional<Path> tryGetPath(String first, String... more) {
if (first == null) return Optional.empty();
try {
return Optional.of(Paths.get(first, more));
} catch (InvalidPathException e) {