Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0867961
经起幽明 悟处通玄
CiiLu May 1, 2026
5b6fc2f
首窥龙堑 见岳见渊
CiiLu May 1, 2026
9269ccc
道不善宣 义不善绻
CiiLu May 1, 2026
f5f4d05
源流万世 大哉乾元
CiiLu May 1, 2026
32b0b77
不曾闻日月争辉 坎离复往立下恒归
CiiLu May 1, 2026
bfb8719
照东南有坤徇乾 承西北
CiiLu May 1, 2026
ca93495
天道自昆仑巍巍 翻起华夏巽震艮兑
CiiLu May 1, 2026
6de972d
万象予万灵得见 两相盈岁
CiiLu May 1, 2026
9ebb8df
潜龙长生应紫微 惟向四方五气寻遂
CiiLu May 1, 2026
965a7a0
燧火旁八卦百草 揆经纬
CiiLu May 1, 2026
d66d952
正位纪天下一归 不消祈天退水
CiiLu May 1, 2026
45922a2
初难知一念一决生龙髓
CiiLu May 1, 2026
fa61fc0
百家注龙慧 千军起龙威 砥淬
CiiLu May 1, 2026
2ce6805
`妙笔生文穗 罡风抚长麾`
CiiLu May 1, 2026
c869953
始见龙形汇 以天田冲腾直向九陲
CiiLu May 1, 2026
8f7dd27
龙震于疆 万里宁壤 天地皆可往
CiiLu May 1, 2026
4c411c4
龙秀于象 引仙来访 诗蜀道河江
CiiLu May 1, 2026
04db7b4
龙明于章 执笔成鉴 映五千煌煌
CiiLu May 1, 2026
e57c3d4
不独九州五岳帝王将相见苍茫
CiiLu May 3, 2026
c678116
龙泽于汤 唤水筑江 单舟见京杭
CiiLu May 3, 2026
5608163
龙健于常 百音同讲 道一种炎黄
CiiLu May 3, 2026
638eb6a
龙景于康 见之庙堂 亦显于曲坊
CiiLu May 3, 2026
f930757
不劳此间祥云瑞兽频频诰春长
CiiLu May 3, 2026
aa6ba20
干支移晷又几回 揽尽天骄襄助一醉
CiiLu May 9, 2026
1aa61a9
虽万言竟道不尽无字碑
CiiLu May 9, 2026
25b3880
临渊乾乾君子催 或跃 无咎相随
CiiLu May 9, 2026
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