feat: support change skin of offline accounts.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,6 +42,10 @@ public class Texture {
|
||||
this.data = requireNonNull(data);
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user