Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 34 additions & 43 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.auth.offline.OfflineSkinConfig;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.skin.TextureModel;
import org.jackhuang.hmcl.game.skin.TextureType;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
Expand All @@ -45,7 +49,10 @@
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
Expand All @@ -65,29 +72,18 @@ private TexturesLoader() {
}

// ==== Texture Loading ====
public static class LoadedTexture {
private final Image image;
private final Map<String, String> metadata;

public record LoadedTexture(Image image, Map<String, String> metadata) {
public LoadedTexture(Image image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}

public Image getImage() {
return image;
}

public Map<String, String> getMetadata() {
return metadata;
}
}

private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins");

private static Path getTexturePath(Texture texture) {
String url = texture.getUrl();
String url = texture.url();
int slash = url.lastIndexOf('/');
int dot = url.lastIndexOf('.');
if (dot < slash) {
Expand All @@ -99,22 +95,22 @@ private static Path getTexturePath(Texture texture) {
}

public static LoadedTexture loadTexture(Texture texture) throws Throwable {
if (StringUtils.isBlank(texture.getUrl())) {
if (StringUtils.isBlank(texture.url())) {
throw new IOException("Texture url is empty");
}

Path file = getTexturePath(texture);
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(texture.getUrl(), file).run();
LOG.info("Texture downloaded: " + texture.getUrl());
new FileDownloadTask(texture.url(), file).run();
LOG.info("Texture downloaded: " + texture.url());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.warning("Failed to download texture " + texture.getUrl() + ", but the file is available", e);
LOG.warning("Failed to download texture " + texture.url() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
throw new IOException("Failed to download texture " + texture.url());
}
}
}
Expand All @@ -127,7 +123,7 @@ public static LoadedTexture loadTexture(Texture texture) throws Throwable {
if (img.isError())
throw img.getException();

Map<String, String> metadata = texture.getMetadata();
Map<String, String> metadata = texture.metadata();
if (metadata == null) {
metadata = emptyMap();
}
Expand Down Expand Up @@ -158,7 +154,7 @@ public static LoadedTexture getDefaultSkin(UUID uuid) {
}

public static TextureModel getDefaultModel(UUID uuid) {
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).metadata().get("model"))
? TextureModel.WIDE
: TextureModel.SLIM;
}
Expand All @@ -176,15 +172,15 @@ public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service,
}
})
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.filter(it -> StringUtils.isNotBlank(it.url())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
LOG.warning("Failed to load texture " + texture.url() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
Expand All @@ -196,28 +192,26 @@ public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service,

public static ObservableValue<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
if (account instanceof OfflineAccount) {
OfflineAccount offlineAccount = (OfflineAccount) account;
if (account instanceof OfflineAccount offlineAccount) {

SimpleObjectProperty<LoadedTexture> binding = new SimpleObjectProperty<>();
InvalidationListener listener = o -> {
Skin skin = offlineAccount.getSkin();
String username = offlineAccount.getUsername();
OfflineSkinConfig skin = offlineAccount.getSkin();

binding.set(uuidFallback);
if (skin != null) {
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
skin.load().setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.warning("Failed to load texture", exception);
} else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) {
} else if (result != null && result.skin() != null && result.skin().image() != null) {
Map<String, String> metadata;
if (result.getModel() != null) {
metadata = singletonMap("model", result.getModel().modelName);
if (result.model() != null) {
metadata = singletonMap("model", result.model().modelName);
} else {
metadata = emptyMap();
}

binding.set(new LoadedTexture(result.getSkin().getImage(), metadata));
binding.set(new LoadedTexture(result.skin().image(), metadata));
}
}).start();
}
Expand All @@ -234,12 +228,12 @@ public static ObservableValue<LoadedTexture> skinBinding(Account account) {
.asyncMap(textures -> {
if (textures.isPresent()) {
Texture texture = textures.get().get(TextureType.SKIN);
if (texture != null && StringUtils.isNotBlank(texture.getUrl())) {
if (texture != null && StringUtils.isNotBlank(texture.url())) {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
LOG.warning("Failed to load texture " + texture.url() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
Expand Down Expand Up @@ -275,15 +269,12 @@ private static void drawAvatar(GraphicsContext g, Image skin, int size, int scal
0, 0, size, size);
}

private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
private record SkinBindingChangeListener(WeakReference<Canvas> canvasRef,
ObservableValue<LoadedTexture> binding) implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();

final WeakReference<Canvas> canvasRef;
final ObservableValue<LoadedTexture> binding;

SkinBindingChangeListener(Canvas canvas, ObservableValue<LoadedTexture> binding) {
this.canvasRef = new WeakReference<>(canvas);
this.binding = binding;
private SkinBindingChangeListener(Canvas canvasRef, ObservableValue<LoadedTexture> binding) {
this(new WeakReference<>(canvasRef), binding);
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public enum SVG {
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"),
APPAREL("m6 10.95l-1.875 1.025l-2.975-5.2L7.75 3H10v1q0 .825.588 1.413T12 6t1.413-.587T14 4V3h2.25l6.6 3.775l-2.95 5.15l-1.9-.95V21H6zM8 7.6V19h8V7.6l3.1 1.7l1.05-1.75l-4.3-2.5q-.375 1.275-1.412 2.113T12 8t-2.437-.837T8.15 5.05l-4.3 2.5L4.9 9.3zm4 4.425"),
BETA_CIRCLE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
Expand All @@ -49,6 +50,7 @@ public enum SVG {
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
CROP_9_16("M9 21q-.825 0-1.412-.587T7 19V5q0-.825.588-1.412T9 3h6q.825 0 1.413.588T17 5v14q0 .825-.587 1.413T15 21zM9 5v14h6V5zm0 0v14z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.skin.TextureType;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.account.skin.OfflineAccountSkinPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
Expand All @@ -55,8 +56,8 @@

import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public class AccountListItem extends RadioButton {

Expand Down Expand Up @@ -138,7 +139,7 @@ public ObservableBooleanValue canUploadSkin() {
@Nullable
public Task<?> uploadSkin() {
if (account instanceof OfflineAccount) {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
Controllers.navigate(new OfflineAccountSkinPage((OfflineAccount) account));
return null;
}
if (!account.canUploadSkin()) {
Expand Down
Loading