From 8c253ac60d12652d6d09c0e5cdf00d1aa8013fbe Mon Sep 17 00:00:00 2001 From: moehreag Date: Thu, 28 Aug 2025 09:30:21 +0200 Subject: [PATCH 01/23] add skin-related API methods --- .../axolotlclient/modules/auth/Auth.java | 2 +- .../axolotlclient/modules/auth/Auth.java | 2 +- .../axolotlclient/modules/auth/Auth.java | 2 +- .../axolotlclient/modules/auth/Auth.java | 2 +- .../axolotlclient/modules/auth/Auth.java | 2 +- .../axolotlclient/modules/auth/MSAuth.java | 118 ++++++++++++++++-- 6 files changed, 112 insertions(+), 16 deletions(-) diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 766b121a3..0c8b347c1 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -61,7 +61,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(AxolotlClient.LOGGER, this, () -> client.options.language); + this.auth = new MSAuth(this, () -> client.options.language); if (isContained(client.getSession().getUuid())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(client.getSession().getUuid())).toList().get(0); current.setAuthToken(client.getSession().getAccessToken()); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index cb5343e47..f968203fa 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -64,7 +64,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(AxolotlClient.LOGGER, this, () -> client.options.language); + this.auth = new MSAuth(this, () -> client.options.language); if (isContained(client.getSession().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid() .equals(UUIDHelper.toUndashed(client.getSession().getPlayerUuid()))).toList().get(0); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 4a041f634..378702109 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -67,7 +67,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(AxolotlClient.LOGGER, this, () -> mc.options.languageCode); + this.auth = new MSAuth(this, () -> mc.options.languageCode); if (isContained(mc.getUser().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(UUIDHelper.toUndashed(mc.getUser().getProfileId()))).toList().getFirst(); if (current.needsRefresh()) { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 521e99c3c..4ffc1eb79 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -63,7 +63,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(AxolotlClient.LOGGER, this, () -> client.options.language); + this.auth = new MSAuth(this, () -> client.options.language); if (isContained(client.getSession().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid() .equals(UUIDHelper.toUndashed(client.getSession().getPlayerUuid()))).toList().getFirst(); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 15af0131a..bfa4e0b21 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -62,7 +62,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(AxolotlClient.LOGGER, this, () -> client.options.language); + this.auth = new MSAuth(this, () -> client.options.language); if (isContained(client.getSession().getUuid())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(client.getSession().getUuid())).toList().get(0); current.setAuthToken(client.getSession().getAccessToken()); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java index 32d90b6bf..1154cded1 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java @@ -22,10 +22,12 @@ package io.github.axolotlclient.modules.auth; +import java.io.FileNotFoundException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; @@ -35,6 +37,8 @@ import java.util.function.Supplier; import com.github.mizosoft.methanol.FormBodyPublisher; +import com.github.mizosoft.methanol.MediaType; +import com.github.mizosoft.methanol.MultipartBodyPublisher; import com.google.gson.JsonObject; import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.util.GsonHelper; @@ -59,11 +63,11 @@ public class MSAuth { public static MSAuth INSTANCE; - public MSAuth(Logger logger, Accounts accounts, Supplier languageSupplier) { - this.logger = logger; + public MSAuth(Accounts accounts, Supplier languageSupplier) { + this.logger = AxolotlClientCommon.getInstance().getLogger(); + this.client = NetworkUtil.createHttpClient(); this.accounts = accounts; this.languageSupplier = languageSupplier; - this.client = getHttpClient(); INSTANCE = this; } @@ -169,7 +173,7 @@ private CompletableFuture> authenticateFromMSTokens(String acc }); } - private record MCProfile(String id, String name, List skins, List capes) { + public record MCProfile(String id, String name, List skins, List capes) { public static MCProfile get(JsonObject json) { return new MCProfile(json.get("id").getAsString(), json.get("name").getAsString(), GsonHelper.jsonArrayToStream(json.getAsJsonArray("skins")) @@ -179,13 +183,38 @@ public static MCProfile get(JsonObject json) { .toList()); } - public record Skin(String id, String state, String url, String variant, String textureKey) { + public record Skin(String id, String state, String url, String variant) { + public static final String VARIANT_CLASSIC = "CLASSIC"; + public static final String VARIANT_SLIM = "SLIM"; + public static final String STATE_ACTIVE = "ACTIVE"; + public static Skin get(JsonObject object) { return new Skin(object.get("id").getAsString(), object.get("state").getAsString(), object.get("url").getAsString(), - object.get("variant").getAsString(), - object.get("textureKey").getAsString()); + object.get("variant").getAsString()); + } + + public CompletableFuture getImage() { + return INSTANCE.client.sendAsync(HttpRequest.newBuilder(URI.create(url())).GET().build(), HttpResponse.BodyHandlers.ofByteArray()) + .thenApplyAsync(res -> { + if (res.statusCode() == 200) { + return res.body(); + } + throw new IllegalArgumentException("anormal status: " + res.statusCode()); + }); + } + + public boolean isClassicVariant() { + return VARIANT_CLASSIC.equals(variant()); + } + + public boolean isSlimVariant() { + return VARIANT_SLIM.equals(variant()); + } + + public boolean isActive() { + return STATE_ACTIVE.equals(state()); } } @@ -193,6 +222,16 @@ public record Cape(String id, String state, String url, String alias) { public static Cape get(JsonObject object) { return new Cape(object.get("id").getAsString(), object.get("state").getAsString(), object.get("url").getAsString(), object.get("alias").getAsString()); } + + public CompletableFuture getImage() { + return INSTANCE.client.sendAsync(HttpRequest.newBuilder(URI.create(url())).GET().build(), HttpResponse.BodyHandlers.ofByteArray()) + .thenApplyAsync(res -> { + if (res.statusCode() == 200) { + return res.body(); + } + throw new IllegalArgumentException("anormal status: " + res.statusCode()); + }); + } } } @@ -263,10 +302,6 @@ private CompletableFuture getMCProfile(String accessToken) { .header("Authorization", "Bearer " + accessToken).build()); } - private HttpClient getHttpClient() { - return NetworkUtil.createHttpClient(); - } - public CompletableFuture> refreshToken(String token, Account account) { return CompletableFuture.supplyAsync(() -> { logger.debug("refreshing auth code... "); @@ -311,4 +346,65 @@ private CompletableFuture requestJson(HttpRequest request) { return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(res -> GsonHelper.fromJson(res.body())); } + + public CompletableFuture> getProfile(Account account) { + return CompletableFuture.supplyAsync(() -> { + JsonObject profileJson = getMCProfile(account.getAuthToken()).join(); + if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { + AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.login.failed", "auth.notif.login.failed.no_profile"); + return Optional.empty(); + } + return Optional.of(MCProfile.get(profileJson)); + }); + } + + public CompletableFuture> setSkin(Account account, MCProfile.Skin skin) { + record Body(String variant, String url) { + } + return requestJson(HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) + .header("Authorization", "Bearer " + account.getAuthToken()) + .POST(HttpRequest.BodyPublishers.ofString(GsonHelper.GSON.toJson(new Body(skin.variant(), skin.url())))).build()) + .thenApply(MCProfile::get).thenApply(Optional::of); + } + + public CompletableFuture> uploadSkin(Account account, Path skin, String variant) { + try { + return requestJson(HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) + .header("Authorization", "Bearer " + account.getAuthToken()) + .POST(MultipartBodyPublisher.newBuilder() + .textPart("variant", variant.toLowerCase(Locale.ROOT)) + .filePart("file", skin, MediaType.IMAGE_PNG).build()).build()) + .thenApply(MCProfile::get).thenApply(Optional::of); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + public CompletableFuture> resetSkin(Account account) { + return requestJson(HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins/activate")) + .header("Authorization", "Bearer " + account.getAuthToken()) + .DELETE().build()) + .thenApply(MCProfile::get).thenApply(Optional::of); + } + + public CompletableFuture> hideCape(Account account) { + return requestJson(HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/activate")) + .header("Authorization", "Bearer " + account.getAuthToken()) + .DELETE().build()) + .thenApply(MCProfile::get).thenApply(Optional::of); + } + + public CompletableFuture> showCape(Account account, MCProfile.Cape cape) { + record Body(String capeId) { + } + return requestJson(HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/activate")) + .header("Authorization", "Bearer " + account.getAuthToken()) + .PUT(HttpRequest.BodyPublishers.ofString(GsonHelper.GSON.toJson(new Body(cape.id())))).build()) + .thenApply(MCProfile::get).thenApply(Optional::of); + } } From 1fe0704fc3ba96fa18ad2b3915957977b85f0a4b Mon Sep 17 00:00:00 2001 From: moehreag Date: Fri, 29 Aug 2025 16:21:06 +0200 Subject: [PATCH 02/23] slight improvements --- .../modules/auth/AccountsScreen.java | 4 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../modules/auth/AccountsScreen.java | 4 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../mixin/GameRendererMixin.java | 14 - .../axolotlclient/mixin/GuiRendererMixin.java | 24 ++ .../modules/auth/AccountsScreen.java | 19 +- .../axolotlclient/modules/auth/Auth.java | 11 +- .../auth/skin/SkinManagementScreen.java | 404 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 121 ++++++ .../modules/auth/skin/SkinRenderState.java | 41 ++ .../modules/auth/skin/SkinRenderer.java | 84 ++++ .../modules/auth/skin/SkinWidget.java | 141 ++++++ .../modules/hud/gui/hud/PlayerHud.java | 8 +- .../hud/util/PlayerHudEntityRenderState.java | 11 +- .../util/IdentifiablePiPRenderState.java | 7 + .../main/resources/axolotlclient.mixins.json | 1 + .../modules/auth/AccountsScreen.java | 4 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../modules/auth/AccountsScreen.java | 4 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../axolotlclient/modules/auth/Account.java | 2 +- .../axolotlclient/modules/auth/Accounts.java | 2 +- .../modules/auth/{MSAuth.java => MSApi.java} | 121 ++++-- .../modules/auth/skin/Asset.java | 17 + .../axolotlclient/modules/auth/skin/Cape.java | 5 + .../axolotlclient/modules/auth/skin/Skin.java | 43 ++ .../assets/axolotlclient/lang/en_us.json | 10 +- 28 files changed, 1025 insertions(+), 101 deletions(-) create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java rename common/src/main/java/io/github/axolotlclient/modules/auth/{MSAuth.java => MSApi.java} (81%) create mode 100644 common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java create mode 100644 common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java create mode 100644 common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 3adb33a3d..02cfbaa42 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -120,14 +120,14 @@ public void removed() { } private void initMSAuth() { - Auth.getInstance().getAuth().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); + Auth.getInstance().getMsApi().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); } private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getAuth()).thenRun(() -> client.execute(() -> { + entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { Auth.getInstance().save(); refresh(); })); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 0c8b347c1..f050ffd26 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -61,7 +61,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(this, () -> client.options.language); + this.msApi = new MSApi(this, () -> client.options.language); if (isContained(client.getSession().getUuid())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(client.getSession().getUuid())).toList().get(0); current.setAuthToken(client.getSession().getAccessToken()); @@ -88,7 +88,7 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(new TranslatableText("auth.notif.title"), new TranslatableText("auth.notif.refreshing", account.getName())); } - account.refresh(auth).thenAccept(res -> { + account.refresh(msApi).thenAccept(res -> { res.ifPresent(a -> { if (!a.isExpired()) { login(a); @@ -143,7 +143,7 @@ void showAccountsExpiredScreen(Account account) { client.execute(() -> client.openScreen(new ConfirmScreen((bl) -> { client.openScreen(current); if (bl) { - auth.startDeviceAuth(); + msApi.startDeviceAuth(); } }, new TranslatableText("auth"), new TranslatableText("auth.accountExpiredNotice", account.getName())))); } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 35b5ee9d9..72df37020 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -125,14 +125,14 @@ public void init() { } private void initMSAuth() { - Auth.getInstance().getAuth().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); + Auth.getInstance().getMsApi().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); } private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getAuth()).thenRun(() -> client.execute(() -> { + entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { Auth.getInstance().save(); refresh(); })); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index f968203fa..bb5719748 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -64,7 +64,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(this, () -> client.options.language); + this.msApi = new MSApi(this, () -> client.options.language); if (isContained(client.getSession().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid() .equals(UUIDHelper.toUndashed(client.getSession().getPlayerUuid()))).toList().get(0); @@ -92,7 +92,7 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Text.translatable("auth.notif.title"), Text.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(auth).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { if (!a.isExpired()) { login(a); } @@ -129,7 +129,7 @@ void showAccountsExpiredScreen(Account account) { client.execute(() -> client.setScreen(new ConfirmScreen((bl) -> { client.setScreen(current); if (bl) { - auth.startDeviceAuth(); + msApi.startDeviceAuth(); } }, Text.translatable("auth"), Text.translatable("auth.accountExpiredNotice", account.getName())))); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java index 556d92737..9e817568b 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java @@ -22,9 +22,6 @@ package io.github.axolotlclient.mixin; -import java.util.ArrayList; -import java.util.List; - import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -35,14 +32,11 @@ import com.mojang.math.Axis; import io.github.axolotlclient.AxolotlClient; import io.github.axolotlclient.modules.blur.MotionBlur; -import io.github.axolotlclient.modules.hud.util.PlayerHudEntityRenderer; import io.github.axolotlclient.modules.zoom.Zoom; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; import org.joml.Matrix4f; @@ -51,7 +45,6 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(GameRenderer.class) @@ -64,13 +57,6 @@ public abstract class GameRendererMixin { @Shadow private boolean panoramicMode; - @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/render/GuiRenderer;(Lnet/minecraft/client/gui/render/state/GuiRenderState;Lnet/minecraft/client/renderer/MultiBufferSource$BufferSource;Ljava/util/List;)V"), index = 2) - private List> addPlayerHudEntityRenderer(List> list, @Local MultiBufferSource.BufferSource source, @Local(argsOnly = true) Minecraft minecraft) { - List> mutable = new ArrayList<>(list); - mutable.add(new PlayerHudEntityRenderer(source, minecraft.getEntityRenderDispatcher())); - return mutable; - } - @WrapOperation(method = "getFov", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Mth;lerp(FFF)F")) private float disableDynamicFov(float delta, float start, float end, Operation original) { if (!AxolotlClient.config().dynamicFOV.get()) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java new file mode 100644 index 000000000..72f8b4f28 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java @@ -0,0 +1,24 @@ +package io.github.axolotlclient.mixin; + +import java.util.Map; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import io.github.axolotlclient.util.IdentifiablePiPRenderState; +import net.minecraft.client.gui.render.GuiRenderer; +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; +import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GuiRenderer.class) +public abstract class GuiRendererMixin { + + @WrapOperation(method = "preparePictureInPictureState", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;")) + private Object improvePiPRenderers(Map, PictureInPictureRenderer> instance, Object o, Operation> original, PictureInPictureRenderState state) { + if (state instanceof IdentifiablePiPRenderState idState) { + return idState.renderer(); + } + return original.call(instance, o); + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index ecd92b156..578cbf949 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth; +import io.github.axolotlclient.modules.auth.skin.SkinManagementScreen; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.ConfirmScreen; @@ -35,6 +36,7 @@ public class AccountsScreen extends Screen { private Button loginButton; private Button deleteButton; private Button refreshButton; + private Button skinsButton; public AccountsScreen(Screen currentScreen) { super(Component.translatable("accounts")); @@ -75,7 +77,12 @@ public void init() { accountsListWidget.setAccounts(Auth.getInstance().getAccounts()); addRenderableWidget(loginButton = Button.builder(Component.translatable("auth.login"), buttonWidget -> login()) - .bounds(this.width / 2 - 154, this.height - 52, 150, 20).build()); + .bounds(this.width / 2 - 154, this.height - 52, 100, 20).build()); + + addRenderableWidget(skinsButton = Button.builder(Component.translatable("skins.manage"), + btn -> minecraft.setScreen(new SkinManagementScreen( + this, accountsListWidget.getSelected().getAccount()))) + .bounds(this.width / 2 - 50, this.height - 52, 100, 20).build()); this.addRenderableWidget(Button.builder(Component.translatable("auth.add"), button -> { if (!Auth.getInstance().allowOfflineAccounts()) { @@ -93,7 +100,7 @@ public void init() { Component.translatable("auth.add.ms") )); } - }).bounds(this.width / 2 + 4, this.height - 52, 150, 20).build()); + }).bounds(this.width / 2 + 4 + 50, this.height - 52, 100, 20).build()); this.deleteButton = this.addRenderableWidget(Button.builder(Component.translatable("selectServer.delete"), button -> { @@ -116,14 +123,14 @@ public void init() { } private void initMSAuth() { - Auth.getInstance().getAuth().startDeviceAuth().thenRun(() -> minecraft.execute(this::refresh)); + Auth.getInstance().getMsApi().startDeviceAuth().thenRun(() -> minecraft.execute(this::refresh)); } private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getAuth()).thenRun(() -> minecraft.execute(() -> { + entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> minecraft.execute(() -> { Auth.getInstance().save(); refresh(); })); @@ -134,9 +141,9 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (minecraft.level == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = true; + deleteButton.active = refreshButton.active = skinsButton.active = true; } else { - loginButton.active = deleteButton.active = refreshButton.active = false; + loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 378702109..93f7992e0 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -37,6 +37,7 @@ import io.github.axolotlclient.mixin.ServerPackManagerAccessor; import io.github.axolotlclient.mixin.SplashManagerAccessor; import io.github.axolotlclient.modules.Module; +import io.github.axolotlclient.modules.auth.skin.SkinManager; import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.notifications.Notifications; import io.github.axolotlclient.util.options.GenericOption; @@ -63,15 +64,17 @@ public class Auth extends Accounts implements Module { private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> mc.setScreen(new AccountsScreen(mc.screen))); private final Set loadingTexture = new HashSet<>(); private final Map textures = new WeakHashMap<>(); + @Getter + private final SkinManager skinManager = new SkinManager(); @Override public void init() { load(); - this.auth = new MSAuth(this, () -> mc.options.languageCode); + this.msApi = new MSApi(this, () -> mc.options.languageCode); if (isContained(mc.getUser().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(UUIDHelper.toUndashed(mc.getUser().getProfileId()))).toList().getFirst(); if (current.needsRefresh()) { - current.refresh(auth).thenRun(this::save); + current.refresh(msApi).thenRun(this::save); } } else { current = new Account(mc.getUser().getName(), UUIDHelper.toUndashed(mc.getUser().getProfileId()), mc.getUser().getAccessToken()); @@ -92,7 +95,7 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Component.translatable("auth.notif.title"), Component.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(auth).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { if (!a.isExpired()) { login(a); } @@ -134,7 +137,7 @@ void showAccountsExpiredScreen(Account account) { mc.execute(() -> mc.setScreen(new ConfirmScreen((bl) -> { mc.setScreen(current); if (bl) { - auth.startDeviceAuth(); + msApi.startDeviceAuth(); } }, Component.translatable("auth"), Component.translatable("auth.accountExpiredNotice", account.getName())))); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java new file mode 100644 index 000000000..909746765 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -0,0 +1,404 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.Watcher; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.*; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinManagementScreen extends Screen { + private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); + private static final int LIST_SKIN_WIDTH = 75; + private static final int LIST_SKIN_HEIGHT = 110; + private final Screen parent; + private final HeaderAndFooterLayout haF = new HeaderAndFooterLayout(this); + private boolean initialized; + private final Account account; + private MSApi.MCProfile cachedProfile; + private SkinListWidget skinList; + private SkinListWidget capesList; + private SkinWidget current; + private final Watcher skinDirWatcher; + + public SkinManagementScreen(Screen parent, Account account) { + super(Component.translatable("skins.manage")); + this.parent = parent; + this.account = account; + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + } + + @Override + protected void init() { + if (!initialized) { + initialized = true; + + haF.addTitleHeader(getTitle(), getFont()); + + haF.addToFooter(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()).build()); + + } + haF.arrangeElements(); + skinList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, skinList.getEntryContentsHeight() + 20); + skinList.setX(width / 2); + capesList.setX(width / 2); + var currentHeight = Math.min((width / 2f) * 120/85, haF.getContentHeight()); + var currentWidth = currentHeight * 85/120; + current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); + current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); + addRenderableWidget(current); + addRenderableWidget(skinList); + addRenderableWidget(capesList); + haF.visitWidgets(this::addRenderableWidget); + capesList.visible = capesList.active = false; + List navBar = new ArrayList<>(); + var skinsTab = addRenderableWidget(Button.builder(Component.translatable("skins.nav.skins"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = true; + capesList.visible = capesList.active = false; + }).pos(width * 3 / 4 - 102, haF.getHeaderHeight()).width(100).build()); + skinsTab.active = false; + navBar.add(skinsTab); + var capesTab = addRenderableWidget(Button.builder(Component.translatable("skins.nav.capes"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = false; + capesList.visible = capesList.active = true; + }).pos(width * 3 / 4 + 2, haF.getHeaderHeight()).width(100).build()); + navBar.add(capesTab); + if (cachedProfile != null) { + initDisplay(); + return; + } + CompletableFuture fut; + if (account.needsRefresh()) { + fut = account.refresh(Auth.getInstance().getMsApi()); + } else { + fut = CompletableFuture.completedFuture(null); + } + fut.thenCompose(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAccept(profile -> { + cachedProfile = profile; + initDisplay(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + var error = Component.translatable("skins.error.failed_to_load"); + var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); + addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); + addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(error), getFont().lineHeight, error, getFont())); + return null; + }); + } + + private void initDisplay() { + loadSkinsList(); + loadCapesList(); + } + + private void loadCapesList() { + capesList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + var capes = profile.capes(); + var deselectCape = createWidgetForCape(current.getSkin(), null); + var activeCape = capes.stream().filter(Cape::isActive).findFirst(); + current.setCape(activeCape.orElse(null)); + deselectCape.noCape(activeCape.isEmpty()); + for (int i = 0; i < capes.size() + 1; i += columns) { + Entry widget; + if (i == 0) { + widget = createEntry(capesList.getEntryContentsHeight(), deselectCape, Component.translatable("skins.capes.no_cape")); + } else { + var cape = capes.get(i - 1); + widget = createEntryForCape(current.getSkin(), cape, capesList.getEntryContentsHeight()); + } + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < capes.size() + 1 - c)) continue; + var cape2 = capes.get(i + c - 1); + Entry widget2 = createEntryForCape(current.getSkin(), cape2, capesList.getEntryContentsHeight()); + + widgets.add(widget2); + } + capesList.addEntry(new Row(widgets)); + } + } + + private void loadSkinsList() { + skinList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + List skins = new ArrayList<>(profile.skins()); + var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var local = new ArrayList<>(loadLocalSkins()); + local.removeIf(s -> hashes.contains(s.textureKey())); + skins.addAll(local); + populateSkinList(skins, columns); + } + + private List loadLocalSkins() { + try { + Files.createDirectories(SKINS_DIR); + try (Stream skins = Files.list(SKINS_DIR)) { + return skins.filter(Files::isRegularFile).sorted(Comparator.comparingLong(p -> { + try { + return Files.getLastModifiedTime(p).toMillis(); + } catch (IOException e) { + return 0L; + } + }).reversed()).map(Auth.getInstance().getSkinManager()::read).filter(Objects::nonNull).toList(); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to read skins dir!", e); + } + return Collections.emptyList(); + } + + private void populateSkinList(List skins, int columns) { + int entryHeight = skinList.getEntryContentsHeight(); + for (int i = 0; i < skins.size(); i += columns) { + var s = skins.get(i); + if (s.isActive()) { + current.setSkin(s); + } + var widget = createEntryForSkin(s, entryHeight); + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < skins.size() - c)) continue; + var s2 = skins.get(i + c); + if (s2.isActive()) { + current.setSkin(s2); + } + var widget2 = createEntryForSkin(s2, entryHeight); + widgets.add(widget2); + } + skinList.addEntry(new Row(widgets)); + } + } + + @Override + public void onFilesDrop(List packs) { + packs.forEach(p -> { + try { + Files.copy(p, SKINS_DIR.resolve(p.getFileName())); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }); + loadSkinsList(); + } + + private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account).darkenIfEquipped()); + } + + private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { + return createEntry(entryHeight, createWidgetForCape(currentSkin, cape), Component.literal(cape.alias())); + } + + private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account).darkenIfEquipped(); + widget2.setRotationY(210); + return widget2; + } + + @Override + protected void clearWidgets() { + super.clearWidgets(); + SkinRenderer.closeRenderers(); + Auth.getInstance().getSkinManager().releaseAll(); + } + + @Override + public void removed() { + Auth.getInstance().getSkinManager().releaseAll(); + Watcher.close(skinDirWatcher); + SkinRenderer.closeRenderers(); + } + + @Override + public void onClose() { + minecraft.setScreen(parent); + } + + private static class SkinListWidget extends ContainerObjectSelectionList { + public SkinListWidget(Minecraft minecraft, int width, int height, int y, int entryHeight) { + super(minecraft, width, height, y, entryHeight); + } + + @Override + public int addEntry(Row entry) { + return super.addEntry(entry); + } + + @Override + protected int scrollBarX() { + return getRight() - 8; + } + + @Override + public int getRowLeft() { + return getX() + 3; + } + + @Override + public int getRowWidth() { + if (!scrollbarVisible()) { + return getWidth() - 4; + } + return getWidth() - 14; + } + + public int getEntryContentsHeight() { + return itemHeight - 4; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + if (!active || !visible) return null; + return super.nextFocusPath(event); + } + + @Override + public void clearEntries() { + super.clearEntries(); + } + } + + private static class Row extends ContainerObjectSelectionList.Entry { + private final List widgets; + + public Row(List entries) { + this.widgets = entries; + } + + @Override + public @NotNull List narratables() { + return widgets; + } + + @Override + public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + int x = left; + if (widgets.isEmpty()) return; + int count = widgets.size(); + int padding = ((width - 5 * (count - 1)) / count); + for (var w : widgets) { + w.setPosition(x, top); + w.setWidth(padding); + w.render(guiGraphics, mouseX, mouseY, partialTick); + x += w.getWidth() + 5; + } + } + + @Override + public @NotNull List children() { + return widgets; + } + } + + Entry createEntry(int height, SkinWidget widget) { + return createEntry(height, widget, null); + } + + Entry createEntry(int height, SkinWidget widget, Component label) { + List widgets = new ArrayList<>(label == null ? 2 : 3); + widgets.add(widget); + if (label != null) { + widgets.add(new AbstractStringWidget(0, 0, widget.getWidth(), 12, label, Minecraft.getInstance().font) { + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + renderScrollingString(guiGraphics, getFont(), 2, -1); + } + }); + } + Button equip = Button.builder(Component.translatable( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> widget.equip().thenAccept(p -> { + this.cachedProfile = p; + initDisplay(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + return null; + })).width(widget.getWidth()).build(); + equip.active = !widget.isEquipped(); + widgets.add(equip); + return new Entry(widget.getWidth(), height, widgets); + } + + private static class Entry extends AbstractContainerWidget { + + private final List widgets; + + private Entry(int width, int height, List widgets) { + super(0, 0, width, height, Component.empty()); + this.widgets = widgets; + } + + @Override + public @NotNull List children() { + return widgets; + } + + @Override + protected int contentHeight() { + return getHeight(); + } + + @Override + protected double scrollRate() { + return 0; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int y = getY() + 4; + for (var w : widgets) { + w.setPosition(getX() + 2, y); + w.setWidth(getWidth() - 4); + w.render(guiGraphics, mouseX, mouseY, partialTick); + y += w.getHeight() + 4; + } + if (isHovered()) { + guiGraphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); + } + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + widgets.forEach(w -> w.updateNarration(narrationElementOutput)); + } + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java new file mode 100644 index 000000000..3d9a16c7b --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -0,0 +1,121 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; + +import com.google.common.hash.Hashing; +import com.mojang.blaze3d.platform.NativeImage; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.AxoMinecraftClient; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.util.ClientColors; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +public class SkinManager { + + private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + + public Skin read(Path p) { + boolean slim; + String sha256; + try { + var in = Files.readAllBytes(p); + sha256 = Hashing.sha256().hashBytes(in).toString(); + try (var img = NativeImage.read(in)) { + slim = (ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0); + } + return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + } + return null; + } + + + public CompletableFuture loadSkin(Skin skin, Account owner) { + var rl = getRl(skin); + if (loadedTextures.contains(rl)) { + return CompletableFuture.completedFuture(rl); + } + + return skin.getImage().thenApplyAsync(bytes -> { + try { + var tex = new DynamicTexture(rl::toString, NativeImage.read(bytes)); + Minecraft.getInstance().getTextureManager().register((ResourceLocation) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((v, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load skin!", t); + } + return v; + }); + } + + public AxoIdentifier loadCape(Cape cape) { + var rl = getRl(cape); + if (loadedTextures.contains(rl)) { + return rl; + } + + return cape.getImage().thenApplyAsync(bytes -> { + try { + var tex = new DynamicTexture(rl::toString, NativeImage.read(bytes)); + Minecraft.getInstance().getTextureManager().register((ResourceLocation) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((id, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load cape!", t); + } + return id; + }).getNow(null); + + } + + public void releaseAll() { + loadedTextures.forEach(id -> Minecraft.getInstance().getTextureManager().release((ResourceLocation) id)); + loadedTextures.clear(); + } + + private @NotNull AxoIdentifier getRl(Skin skin) { + return AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + Hashing.sha256().hashUnencodedChars(skin.id())); + } + + private @NotNull AxoIdentifier getRl(Cape cape) { + return AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + Hashing.sha256().hashUnencodedChars(cape.id())); + } + + public String getDefaultSkinHash(Account account) { + var skin = DefaultPlayerSkin.get(UUIDHelper.fromUndashed(account.getUuid())); + var mc = Minecraft.getInstance(); + var resourceManager = mc.getResourceManager(); + try { + var res = resourceManager.getResourceOrThrow(skin.texture()); + try ( + var in = res.open()) { + return Hashing.sha256().hashBytes(in.readAllBytes()).toString(); + } + } catch (IOException ignored) { + } + return null; + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java new file mode 100644 index 000000000..fd154ce16 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java @@ -0,0 +1,41 @@ +package io.github.axolotlclient.modules.auth.skin; + +import io.github.axolotlclient.util.IdentifiablePiPRenderState; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +public record SkinRenderState(boolean classicVariant, + ResourceLocation skinTexture, + @Nullable ResourceLocation cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale, + @Nullable ScreenRectangle scissorArea, + @Nullable ScreenRectangle bounds, + SkinRenderer renderer, + int color) implements PictureInPictureRenderState, IdentifiablePiPRenderState { + + public SkinRenderState(boolean classicVariant, + ResourceLocation skinTexture, + @Nullable ResourceLocation cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale, + @Nullable ScreenRectangle scissorArea, + SkinRenderer renderer, + int color) { + this(classicVariant, skinTexture, cape, rotationX, rotationY, pivotY, x0, y0, x1, y1, scale, scissorArea, PictureInPictureRenderState.getBounds(x0, y0, x1, y1, scissorArea), renderer, color); + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java new file mode 100644 index 000000000..5281c0d44 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -0,0 +1,84 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; +import net.minecraft.client.model.PlayerCapeModel; +import net.minecraft.client.model.PlayerModel; +import net.minecraft.client.model.geom.ModelLayers; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.state.PlayerRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4fStack; + +public class SkinRenderer extends PictureInPictureRenderer { + private static final Map renderers = new ConcurrentHashMap<>(); + + public static void closeRenderers() { + renderers.values().forEach(PictureInPictureRenderer::close); + renderers.clear(); + } + + public static SkinRenderer getOrCreate(MultiBufferSource.BufferSource bufferSource, Minecraft minecraft, String id) { + return renderers.computeIfAbsent(id, _id -> new SkinRenderer(bufferSource, minecraft, id)); + } + + private PlayerModel classicModel, slimModel; + private PlayerCapeModel capeModel; + private final Minecraft minecraft; + private final String id; + + private SkinRenderer(MultiBufferSource.BufferSource bufferSource, Minecraft minecraft, String id) { + super(bufferSource); + this.minecraft = minecraft; + this.id = id; + } + + @Override + public @NotNull Class getRenderStateClass() { + return SkinRenderState.class; + } + + @Override + protected void renderToTexture(SkinRenderState renderState, PoseStack poseStack) { + if (classicModel == null) { + classicModel = new PlayerModel(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER), false); + } + if (slimModel == null) { + slimModel = new PlayerModel(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER_SLIM), true); + } + Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.PLAYER_SKIN); + int i = Minecraft.getInstance().getWindow().getGuiScale(); + Matrix4fStack matrix4fStack = RenderSystem.getModelViewStack(); + matrix4fStack.pushMatrix(); + float f = renderState.scale() * i; + matrix4fStack.rotateAround(Axis.XP.rotationDegrees(renderState.rotationX()), 0.0F, f * -renderState.pivotY(), 0.0F); + poseStack.mulPose(Axis.YP.rotationDegrees(-renderState.rotationY())); + poseStack.translate(0.0F, -1.6010001F, 0.0F); + var model = renderState.classicVariant() ? classicModel : slimModel; + RenderType renderType = model.renderType(renderState.skinTexture()); + model.renderToBuffer(poseStack, this.bufferSource.getBuffer(renderType), 15728880, OverlayTexture.NO_OVERLAY, renderState.color()); + if (renderState.cape() != null) { + if (capeModel == null) { + capeModel = new PlayerCapeModel<>(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER_CAPE)); + } + var type = capeModel.renderType(renderState.cape()); + capeModel.renderToBuffer(poseStack, bufferSource.getBuffer(type), 15728880, OverlayTexture.NO_OVERLAY, renderState.color()); + } + this.bufferSource.endBatch(); + matrix4fStack.popMatrix(); + } + + @Override + protected @NotNull String getTextureLabel() { + return "axolotlclient/skin_render/" + id; + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java new file mode 100644 index 000000000..7fc8e216d --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -0,0 +1,141 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.GuiGraphicsAccessor; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.client.resources.PlayerSkin; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +public class SkinWidget extends AbstractWidget { + private static final float MODEL_HEIGHT = 2.125F; + private static final float FIT_SCALE = 0.97F; + private static final float ROTATION_SENSITIVITY = 2.5F; + private static final float DEFAULT_ROTATION_X = -5.0F; + private static final float DEFAULT_ROTATION_Y = 30.0F; + private static final float ROTATION_X_LIMIT = 50.0F; + private float rotationX = DEFAULT_ROTATION_X; + @Setter + private float rotationY = DEFAULT_ROTATION_Y; + @Getter + @Setter + private Skin skin; + @Getter + @Setter + private Cape cape; + private final Account owner; + private boolean noCape, noCapeActive; + private boolean darkenIfEquipped; + + public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { + super(0, 0, width, height, CommonComponents.EMPTY); + this.skin = skin; + this.cape = cape; + this.owner = owner; + } + + public SkinWidget(int width, int height, Skin skin, Account owner) { + this(width, height, skin, null, owner); + } + + public void noCape(boolean noCapeActive) { + noCape = true; + this.noCapeActive = noCapeActive; + } + + public SkinWidget darkenIfEquipped() { + darkenIfEquipped = true; + return this; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + var minecraft = Minecraft.getInstance(); + + float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; + float pivotY = -1.0625F; + + AxoIdentifier skinRl; + int col; + boolean classic; + SkinManager skinManager = Auth.getInstance().getSkinManager(); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin, owner); + if (loader != null && loader.isDone()) { + skinRl = loader.join(); + col = darkenIfEquipped && isEquipped() ? ARGB.setBrightness(-1, 0.4f) : -1; + classic = skin.isClassicVariant(); + } else { + col = ARGB.setBrightness(-1, 0.6f); + var skin = DefaultPlayerSkin.get(UUIDHelper.fromUndashed(owner.getUuid())); + classic = skin.model() == PlayerSkin.Model.WIDE; + skinRl = skin.texture(); + } + var capeRl = cape == null ? null : skinManager.loadCape(cape); + + var renderer = SkinRenderer.getOrCreate(minecraft.renderBuffers().bufferSource(), minecraft, "" + hashCode()); + ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState() + .submitPicturesInPictureState( + new SkinRenderState(classic, (ResourceLocation) skinRl, (ResourceLocation) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getRight(), this.getBottom(), scale, guiGraphics.scissorStack.peek(), renderer, col)); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) { + this.rotationX = Mth.clamp(this.rotationX - (float) dragY * ROTATION_SENSITIVITY, -ROTATION_X_LIMIT, ROTATION_X_LIMIT); + this.rotationY += (float) dragX * ROTATION_SENSITIVITY; + } + + @Override + public void playDownSound(SoundManager handler) { + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { + } + + @Override + public boolean isActive() { + return false; + } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent event) { + return null; + } + + public boolean isEquipped() { + return noCape ? noCapeActive : (cape != null ? cape.isActive() : skin == null || skin.isActive()); + } + + public CompletableFuture equip() { + var msApi = Auth.getInstance().getMsApi(); + if (noCape) { + return msApi.hideCape(owner); + } + if (cape != null) { + return cape.equip(msApi, owner); + } + if (skin != null) { + return skin.equip(msApi, owner); + } + return msApi.resetSkin(owner); + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/gui/hud/PlayerHud.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/gui/hud/PlayerHud.java index 004bff5bf..a6a616748 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/gui/hud/PlayerHud.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/gui/hud/PlayerHud.java @@ -27,6 +27,7 @@ import io.github.axolotlclient.bridge.render.AxoRenderContext; import io.github.axolotlclient.mixin.GuiGraphicsAccessor; import io.github.axolotlclient.modules.hud.util.PlayerHudEntityRenderState; +import io.github.axolotlclient.modules.hud.util.PlayerHudEntityRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.player.LocalPlayer; @@ -49,6 +50,7 @@ public class PlayerHud extends PlayerHudCommon { private LivingEntityRenderState reusedPlayerRendererState = null; + private PlayerHudEntityRenderer renderer; public PlayerHud() { super(); @@ -148,7 +150,9 @@ private void renderEntityInInventory( @Nullable Quaternionf quaternionf2, LivingEntity livingEntity ) { - EntityRenderDispatcher entityRenderDispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + Minecraft mc = Minecraft.getInstance(); + EntityRenderDispatcher entityRenderDispatcher = mc.getEntityRenderDispatcher(); + if (renderer == null) renderer = new PlayerHudEntityRenderer(mc.renderBuffers().bufferSource(), entityRenderDispatcher); EntityRenderer entityRenderer = (EntityRenderer) entityRenderDispatcher.getRenderer(livingEntity); if (reusedPlayerRendererState == null) { reusedPlayerRendererState = entityRenderer.createRenderState(); @@ -156,7 +160,7 @@ private void renderEntityInInventory( entityRenderer.extractRenderState(livingEntity, reusedPlayerRendererState, 1.0f); reusedPlayerRendererState.nameTag = null; reusedPlayerRendererState.hitboxesRenderState = null; - ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState().submitPicturesInPictureState(new PlayerHudEntityRenderState(reusedPlayerRendererState, vector3f, quaternionf, quaternionf2, i, j, k, l, f, ((GuiGraphicsAccessor) guiGraphics).getScissorStack().peek())); + ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState().submitPicturesInPictureState(new PlayerHudEntityRenderState(reusedPlayerRendererState, vector3f, quaternionf, quaternionf2, i, j, k, l, f, ((GuiGraphicsAccessor) guiGraphics).getScissorStack().peek(), renderer)); } private boolean isPerformingAction() { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/util/PlayerHudEntityRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/util/PlayerHudEntityRenderState.java index 2038087f9..3257cb423 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/util/PlayerHudEntityRenderState.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/util/PlayerHudEntityRenderState.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.hud.util; +import io.github.axolotlclient.util.IdentifiablePiPRenderState; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState; import net.minecraft.client.renderer.entity.state.EntityRenderState; @@ -39,7 +40,8 @@ public record PlayerHudEntityRenderState(EntityRenderState renderState, int y1, float scale, @Nullable ScreenRectangle scissorArea, - @Nullable ScreenRectangle bounds) implements PictureInPictureRenderState { + @Nullable ScreenRectangle bounds, + PlayerHudEntityRenderer renderer) implements PictureInPictureRenderState, IdentifiablePiPRenderState { public PlayerHudEntityRenderState( EntityRenderState entityRenderState, @@ -51,11 +53,10 @@ public PlayerHudEntityRenderState( int k, int l, float f, - @Nullable ScreenRectangle screenRectangle + @Nullable ScreenRectangle screenRectangle, + PlayerHudEntityRenderer renderer ) { - this( - entityRenderState, vector3f, quaternionf, quaternionf2, i, j, k, l, f, screenRectangle, PictureInPictureRenderState.getBounds(i, j, k, l, screenRectangle) - ); + this(entityRenderState, vector3f, quaternionf, quaternionf2, i, j, k, l, f, screenRectangle, PictureInPictureRenderState.getBounds(i, j, k, l, screenRectangle), renderer); } } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java new file mode 100644 index 000000000..905881a62 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java @@ -0,0 +1,7 @@ +package io.github.axolotlclient.util; + +import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; + +public interface IdentifiablePiPRenderState> { + T renderer(); +} diff --git a/1.21.7/src/main/resources/axolotlclient.mixins.json b/1.21.7/src/main/resources/axolotlclient.mixins.json index 1c53a17ec..bf43bb69a 100644 --- a/1.21.7/src/main/resources/axolotlclient.mixins.json +++ b/1.21.7/src/main/resources/axolotlclient.mixins.json @@ -26,6 +26,7 @@ "GameRendererMixin", "GuiGraphicsAccessor", "GuiGraphicsMixin", + "GuiRendererMixin", "HandledScreenMixin", "InGameHudMixin", "InGameOverlayRendererMixin", diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index ef81f4214..ba7008639 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -120,14 +120,14 @@ public void init() { } private void initMSAuth() { - Auth.getInstance().getAuth().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); + Auth.getInstance().getMsApi().startDeviceAuth().thenRun(() -> client.execute(this::refresh)); } private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getAuth()).thenRun(() -> client.execute(() -> { + entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { Auth.getInstance().save(); refresh(); })); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 4ffc1eb79..bf4acb06c 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -63,7 +63,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(this, () -> client.options.language); + this.msApi = new MSApi(this, () -> client.options.language); if (isContained(client.getSession().getSessionId())) { current = getAccounts().stream().filter(account -> account.getUuid() .equals(UUIDHelper.toUndashed(client.getSession().getPlayerUuid()))).toList().getFirst(); @@ -91,7 +91,7 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Text.translatable("auth.notif.title"), Text.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(auth).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { if (!a.isExpired()) { login(a); } @@ -129,7 +129,7 @@ void showAccountsExpiredScreen(Account account) { client.execute(() -> client.setScreen(new ConfirmScreen((bl) -> { client.setScreen(current); if (bl) { - auth.startDeviceAuth(); + msApi.startDeviceAuth(); } }, Text.translatable("auth"), Text.translatable("auth.accountExpiredNotice", account.getName())))); } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 84b0896a3..6ebf5451e 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -205,14 +205,14 @@ private void login() { } private void initMSAuth() { - Auth.getInstance().getAuth().startDeviceAuth().thenRun(() -> minecraft.submit(this::refresh)); + Auth.getInstance().getMsApi().startDeviceAuth().thenRun(() -> minecraft.submit(this::refresh)); } private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedEntry(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getAuth()).thenRun(() -> minecraft.submit(() -> { + entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> minecraft.submit(() -> { Auth.getInstance().save(); refresh(); })); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index bfa4e0b21..4ec787ec3 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -62,7 +62,7 @@ public class Auth extends Accounts implements Module { @Override public void init() { load(); - this.auth = new MSAuth(this, () -> client.options.language); + this.msApi = new MSApi(this, () -> client.options.language); if (isContained(client.getSession().getUuid())) { current = getAccounts().stream().filter(account -> account.getUuid().equals(client.getSession().getUuid())).toList().get(0); current.setAuthToken(client.getSession().getAccessToken()); @@ -89,7 +89,7 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus("auth.notif.title", "auth.notif.refreshing", account.getName()); } - account.refresh(auth).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { if (!a.isExpired()) { login(a); } @@ -145,7 +145,7 @@ void showAccountsExpiredScreen(Account account) { client.submit(() -> client.openScreen(new ConfirmScreen((bl, i) -> { client.openScreen(current); if (bl) { - auth.startDeviceAuth(); + msApi.startDeviceAuth(); } }, I18n.translate("auth"), I18n.translate("auth.accountExpiredNotice", account.getName()), 1))); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java index f7e759496..fdfb417f3 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java @@ -83,7 +83,7 @@ public static Account deserialize(JsonObject object) { return new Account(uuid, name, authToken, msaToken, refreshToken, expiration); } - public CompletableFuture> refresh(MSAuth auth) { + public CompletableFuture> refresh(MSApi auth) { return auth.refreshToken(refreshToken, this); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java index 1b9d8667d..93e0ab0a8 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java @@ -40,7 +40,7 @@ public abstract class Accounts { private final List accounts = new ArrayList<>(); protected Account current; - protected MSAuth auth; + protected MSApi msApi; public void load() { Path legacy = AxolotlClientCommon.resolveConfigFile("../accounts.json"); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java similarity index 81% rename from common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java rename to common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index 1154cded1..73f493be2 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSAuth.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -27,7 +27,6 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; @@ -39,14 +38,18 @@ import com.github.mizosoft.methanol.FormBodyPublisher; import com.github.mizosoft.methanol.MediaType; import com.github.mizosoft.methanol.MultipartBodyPublisher; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.modules.auth.skin.Cape; +import io.github.axolotlclient.modules.auth.skin.Skin; import io.github.axolotlclient.util.GsonHelper; import io.github.axolotlclient.util.Logger; import io.github.axolotlclient.util.NetworkUtil; // Partly oriented on In-Game-Account-Switcher by The-Fireplace, VidTu -public class MSAuth { +public class MSApi { private static final String CLIENT_ID = "938592fc-8e01-4c6d-b56d-428c7d9cf5ea"; // AxolotlClient MSA ClientID private static final String SCOPES = "XboxLive.signin offline_access"; @@ -60,10 +63,11 @@ public class MSAuth { private final Logger logger; private final Accounts accounts; private final HttpClient client; + private final Gson gson = new GsonBuilder().create(); - public static MSAuth INSTANCE; + public static MSApi INSTANCE; - public MSAuth(Accounts accounts, Supplier languageSupplier) { + public MSApi(Accounts accounts, Supplier languageSupplier) { this.logger = AxolotlClientCommon.getInstance().getLogger(); this.client = NetworkUtil.createHttpClient(); this.accounts = accounts; @@ -173,26 +177,28 @@ private CompletableFuture> authenticateFromMSTokens(String acc }); } - public record MCProfile(String id, String name, List skins, List capes) { + public record MCProfile(String id, String name, List skins, List capes) { public static MCProfile get(JsonObject json) { return new MCProfile(json.get("id").getAsString(), json.get("name").getAsString(), GsonHelper.jsonArrayToStream(json.getAsJsonArray("skins")) - .map(s -> Skin.get(s.getAsJsonObject())) + .map(s -> OnlineSkin.get(s.getAsJsonObject())) .toList(), GsonHelper.jsonArrayToStream(json.getAsJsonArray("capes")) - .map(s -> Cape.get(s.getAsJsonObject())) + .map(s -> OnlineCape.get(s.getAsJsonObject())) .toList()); } - public record Skin(String id, String state, String url, String variant) { + public record OnlineSkin(String id, String state, String url, String variant, String textureKey) implements Skin { public static final String VARIANT_CLASSIC = "CLASSIC"; public static final String VARIANT_SLIM = "SLIM"; public static final String STATE_ACTIVE = "ACTIVE"; - public static Skin get(JsonObject object) { - return new Skin(object.get("id").getAsString(), + public static OnlineSkin get(JsonObject object) { + String url = object.get("url").getAsString(); + return new OnlineSkin(object.get("id").getAsString(), object.get("state").getAsString(), - object.get("url").getAsString(), - object.get("variant").getAsString()); + url, + object.get("variant").getAsString(), + url.substring(url.lastIndexOf("/")+1)); } public CompletableFuture getImage() { @@ -201,7 +207,7 @@ public CompletableFuture getImage() { if (res.statusCode() == 200) { return res.body(); } - throw new IllegalArgumentException("anormal status: " + res.statusCode()); + throw new IllegalArgumentException("abnormal status: " + res.statusCode()); }); } @@ -216,11 +222,20 @@ public boolean isSlimVariant() { public boolean isActive() { return STATE_ACTIVE.equals(state()); } + + @Override + public CompletableFuture equip(MSApi api, Account account) { + return api.setSkin(account, this); + } } - public record Cape(String id, String state, String url, String alias) { - public static Cape get(JsonObject object) { - return new Cape(object.get("id").getAsString(), object.get("state").getAsString(), object.get("url").getAsString(), object.get("alias").getAsString()); + public record OnlineCape(String id, String state, String url, String alias, String textureKey) implements Cape { + public static final String STATE_ACTIVE = "ACTIVE"; + + public static OnlineCape get(JsonObject object) { + String url = object.get("url").getAsString(); + return new OnlineCape(object.get("id").getAsString(), object.get("state").getAsString(), + url, object.get("alias").getAsString(), url.substring(url.lastIndexOf("/")+1)); } public CompletableFuture getImage() { @@ -229,9 +244,18 @@ public CompletableFuture getImage() { if (res.statusCode() == 200) { return res.body(); } - throw new IllegalArgumentException("anormal status: " + res.statusCode()); + throw new IllegalArgumentException("abnormal status: " + res.statusCode()); }); } + + public boolean isActive() { + return STATE_ACTIVE.equals(state()); + } + + @Override + public CompletableFuture equip(MSApi api, Account account) { + return api.showCape(account, this); + } } } @@ -347,64 +371,67 @@ private CompletableFuture requestJson(HttpRequest request) { .thenApply(res -> GsonHelper.fromJson(res.body())); } - public CompletableFuture> getProfile(Account account) { - return CompletableFuture.supplyAsync(() -> { - JsonObject profileJson = getMCProfile(account.getAuthToken()).join(); - if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { - AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.login.failed", "auth.notif.login.failed.no_profile"); - return Optional.empty(); - } - return Optional.of(MCProfile.get(profileJson)); - }); + public CompletableFuture getProfile(Account account) { + return getMCProfile(account.getAuthToken()).thenApply(this::extractProfile); } - public CompletableFuture> setSkin(Account account, MCProfile.Skin skin) { + private MCProfile extractProfile(JsonObject profileJson) { + if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { + throw new IllegalStateException("profile not found"); + } + return MCProfile.get(profileJson); + } + + public CompletableFuture setSkin(Account account, MCProfile.OnlineSkin skin) { record Body(String variant, String url) { } return requestJson(HttpRequest.newBuilder() .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) .header("Authorization", "Bearer " + account.getAuthToken()) - .POST(HttpRequest.BodyPublishers.ofString(GsonHelper.GSON.toJson(new Body(skin.variant(), skin.url())))).build()) - .thenApply(MCProfile::get).thenApply(Optional::of); + .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(new Body(skin.variant(), skin.url())))).build()) + .thenApply(this::extractProfile); } - public CompletableFuture> uploadSkin(Account account, Path skin, String variant) { + public CompletableFuture uploadAndSetSkin(Account account, Skin.Local skin) { try { return requestJson(HttpRequest.newBuilder() .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) .header("Authorization", "Bearer " + account.getAuthToken()) .POST(MultipartBodyPublisher.newBuilder() - .textPart("variant", variant.toLowerCase(Locale.ROOT)) - .filePart("file", skin, MediaType.IMAGE_PNG).build()).build()) - .thenApply(MCProfile::get).thenApply(Optional::of); + .textPart("variant", skin.isClassicVariant() ? "classic" : "slim") + .filePart("file", skin.file(), MediaType.IMAGE_PNG).build()).build()) + .thenApply(this::extractProfile); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } - public CompletableFuture> resetSkin(Account account) { - return requestJson(HttpRequest.newBuilder() - .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins/activate")) + public CompletableFuture resetSkin(Account account) { + return requestJson(HttpRequest.newBuilder().DELETE() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins/active")) .header("Authorization", "Bearer " + account.getAuthToken()) - .DELETE().build()) - .thenApply(MCProfile::get).thenApply(Optional::of); + .build()) + .thenApply(this::extractProfile); } - public CompletableFuture> hideCape(Account account) { - return requestJson(HttpRequest.newBuilder() - .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/activate")) + public CompletableFuture hideCape(Account account) { + return requestJson(HttpRequest.newBuilder().DELETE() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/active")) .header("Authorization", "Bearer " + account.getAuthToken()) - .DELETE().build()) - .thenApply(MCProfile::get).thenApply(Optional::of); + .build()) + .thenApply(js -> { + return js; + }) + .thenApply(this::extractProfile); } - public CompletableFuture> showCape(Account account, MCProfile.Cape cape) { + public CompletableFuture showCape(Account account, MCProfile.OnlineCape cape) { record Body(String capeId) { } return requestJson(HttpRequest.newBuilder() - .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/activate")) + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/active")) .header("Authorization", "Bearer " + account.getAuthToken()) - .PUT(HttpRequest.BodyPublishers.ofString(GsonHelper.GSON.toJson(new Body(cape.id())))).build()) - .thenApply(MCProfile::get).thenApply(Optional::of); + .PUT(HttpRequest.BodyPublishers.ofString(gson.toJson(new Body(cape.id())))).build()) + .thenApply(this::extractProfile); } } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java new file mode 100644 index 000000000..35f62e964 --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java @@ -0,0 +1,17 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.MSApi; + +public interface Asset { + String id(); + CompletableFuture getImage(); + + boolean isActive(); + + CompletableFuture equip(MSApi api, Account account); + + String textureKey(); +} diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java new file mode 100644 index 000000000..0275f0fcb --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java @@ -0,0 +1,5 @@ +package io.github.axolotlclient.modules.auth.skin; + +public interface Cape extends Asset { + String alias(); +} diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java new file mode 100644 index 000000000..3478a5bac --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -0,0 +1,43 @@ +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.MSApi; + +public interface Skin extends Asset { + boolean isClassicVariant(); + + record Local(boolean classic, String id, Path file, String textureKey) implements Skin { + + @Override + public boolean isClassicVariant() { + return classic; + } + + @Override + public CompletableFuture getImage() { + return CompletableFuture.supplyAsync(() -> { + try { + return Files.readAllBytes(file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public CompletableFuture equip(MSApi api, Account account) { + return api.uploadAndSetSkin(account, this); + } + } +} diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index 76e1914ea..6f49cf8d4 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -811,5 +811,13 @@ "bedwars.stats_overlay.auto_activate.tooltip": "Automatically activate on game start. Requires the overlay to be in toggle mode.", "inventoryhud.item_background": "Item Background", "inventoryhud.item_background_color": "Item Background Color", - "inventoryhud.always_show_item_backgrounds": "Always Show Item Backgrounds" + "inventoryhud.always_show_item_backgrounds": "Always Show Item Backgrounds", + "skins.manage": "Manage Skins", + "skins.nav.skins": "Skins", + "skins.nav.capes": "Capes", + "skins.error.failed_to_load": "Failed to load skins", + "skins.error.failed_to_load_desc": "Your log file may include more information.", + "skins.capes.no_cape": "No Cape", + "skins.manage.equipped": "Equipped", + "skins.manage.equip": "Equip" } From dfbd446cecda534cb68229f50355257b4af03259 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 30 Aug 2025 00:49:43 +0200 Subject: [PATCH 03/23] improve loading, add default skin option --- .../auth/skin/SkinManagementScreen.java | 64 +++++++++------ .../modules/auth/skin/SkinWidget.java | 77 ++++++++++++++++--- .../assets/axolotlclient/lang/en_us.json | 3 +- gradle.properties | 2 +- 4 files changed, 113 insertions(+), 33 deletions(-) diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 909746765..813690c10 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -9,7 +9,6 @@ import java.util.stream.Stream; import io.github.axolotlclient.AxolotlClientCommon; -import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; @@ -25,7 +24,6 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import org.jetbrains.annotations.NotNull; @@ -42,6 +40,7 @@ public class SkinManagementScreen extends Screen { private MSApi.MCProfile cachedProfile; private SkinListWidget skinList; private SkinListWidget capesList; + private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; @@ -58,46 +57,62 @@ protected void init() { initialized = true; haF.addTitleHeader(getTitle(), getFont()); - haF.addToFooter(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()).build()); - } haF.arrangeElements(); + var loadingPlaceholder = new LoadingDotsWidget(getFont(), Component.translatable("skins.loading")); + loadingPlaceholder.setRectangle(width, haF.getContentHeight(), 0, + haF.getHeaderHeight()); + addRenderableWidget(loadingPlaceholder); skinList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, skinList.getEntryContentsHeight() + 20); skinList.setX(width / 2); capesList.setX(width / 2); - var currentHeight = Math.min((width / 2f) * 120/85, haF.getContentHeight()); - var currentWidth = currentHeight * 85/120; + var currentHeight = Math.min((width / 2f) * 120 / 85, haF.getContentHeight()); + var currentWidth = currentHeight * 85 / 120; current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); - addRenderableWidget(current); - addRenderableWidget(skinList); - addRenderableWidget(capesList); - haF.visitWidgets(this::addRenderableWidget); - capesList.visible = capesList.active = false; + + if (!capesTab) { + capesList.visible = capesList.active = false; + } else { + skinList.visible = skinList.active = false; + } List navBar = new ArrayList<>(); - var skinsTab = addRenderableWidget(Button.builder(Component.translatable("skins.nav.skins"), btn -> { + var skinsTab = Button.builder(Component.translatable("skins.nav.skins"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); btn.active = false; skinList.visible = skinList.active = true; capesList.visible = capesList.active = false; - }).pos(width * 3 / 4 - 102, haF.getHeaderHeight()).width(100).build()); - skinsTab.active = false; + capesTab = false; + }).pos(width * 3 / 4 - 102, haF.getHeaderHeight()).width(100).build(); navBar.add(skinsTab); - var capesTab = addRenderableWidget(Button.builder(Component.translatable("skins.nav.capes"), btn -> { + var capesTab = Button.builder(Component.translatable("skins.nav.capes"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); btn.active = false; skinList.visible = skinList.active = false; capesList.visible = capesList.active = true; - }).pos(width * 3 / 4 + 2, haF.getHeaderHeight()).width(100).build()); + this.capesTab = true; + }).pos(width * 3 / 4 + 2, haF.getHeaderHeight()).width(100).build(); navBar.add(capesTab); + skinsTab.active = this.capesTab; + capesTab.active = !this.capesTab; + Runnable addWidgets = () -> { + removeWidget(loadingPlaceholder); + addRenderableWidget(current); + addRenderableWidget(skinsTab); + addRenderableWidget(capesTab); + addRenderableWidget(skinList); + addRenderableWidget(capesList); + haF.visitWidgets(this::addRenderableWidget); + }; if (cachedProfile != null) { initDisplay(); + addWidgets.run(); return; } CompletableFuture fut; @@ -110,12 +125,13 @@ protected void init() { .thenAccept(profile -> { cachedProfile = profile; initDisplay(); + addWidgets.run(); }).exceptionally(t -> { AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = Component.translatable("skins.error.failed_to_load"); var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); - addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(error), getFont().lineHeight, error, getFont())); + addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(errorDesc), getFont().lineHeight, errorDesc, getFont())); return null; }); } @@ -161,6 +177,10 @@ private void loadSkinsList() { int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); List skins = new ArrayList<>(profile.skins()); var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } var local = new ArrayList<>(loadLocalSkins()); local.removeIf(s -> hashes.contains(s.textureKey())); skins.addAll(local); @@ -189,7 +209,7 @@ private void populateSkinList(List skins, int columns) { int entryHeight = skinList.getEntryContentsHeight(); for (int i = 0; i < skins.size(); i += columns) { var s = skins.get(i); - if (s.isActive()) { + if (s != null && s.isActive()) { current.setSkin(s); } var widget = createEntryForSkin(s, entryHeight); @@ -198,7 +218,7 @@ private void populateSkinList(List skins, int columns) { for (int c = 1; c < columns; c++) { if (!(i < skins.size() - c)) continue; var s2 = skins.get(i + c); - if (s2.isActive()) { + if (s2 != null && s2.isActive()) { current.setSkin(s2); } var widget2 = createEntryForSkin(s2, entryHeight); @@ -221,7 +241,7 @@ public void onFilesDrop(List packs) { } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { - return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account).darkenIfEquipped()); + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account).highlightIfEquipped()); } private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { @@ -229,7 +249,7 @@ public void onFilesDrop(List packs) { } private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { - SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account).darkenIfEquipped(); + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account).highlightIfEquipped(); widget2.setRotationY(210); return widget2; } @@ -384,7 +404,7 @@ protected double scrollRate() { @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - int y = getY() + 4; + int y = getY()+4; for (var w : widgets) { w.setPosition(getX() + 2, y); w.setWidth(getWidth() - 4); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 7fc8e216d..b1d4ea5d8 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -2,12 +2,16 @@ import java.util.concurrent.CompletableFuture; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.vertex.VertexConsumer; import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.GameRendererAccessor; import io.github.axolotlclient.mixin.GuiGraphicsAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.ClientColors; import lombok.Getter; import lombok.Setter; import net.minecraft.client.Minecraft; @@ -16,14 +20,18 @@ import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.render.TextureSetup; +import net.minecraft.client.gui.render.state.GuiElementRenderState; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.client.resources.PlayerSkin; import net.minecraft.client.sounds.SoundManager; import net.minecraft.network.chat.CommonComponents; import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.ARGB; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; +import org.joml.Matrix3x2f; public class SkinWidget extends AbstractWidget { private static final float MODEL_HEIGHT = 2.125F; @@ -43,7 +51,7 @@ public class SkinWidget extends AbstractWidget { private Cape cape; private final Account owner; private boolean noCape, noCapeActive; - private boolean darkenIfEquipped; + private boolean highlightIfEquipped; public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { super(0, 0, width, height, CommonComponents.EMPTY); @@ -61,8 +69,8 @@ public void noCape(boolean noCapeActive) { this.noCapeActive = noCapeActive; } - public SkinWidget darkenIfEquipped() { - darkenIfEquipped = true; + public SkinWidget highlightIfEquipped() { + highlightIfEquipped = true; return this; } @@ -74,26 +82,27 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo float pivotY = -1.0625F; AxoIdentifier skinRl; - int col; boolean classic; + boolean equipped = isEquipped(); SkinManager skinManager = Auth.getInstance().getSkinManager(); CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin, owner); if (loader != null && loader.isDone()) { skinRl = loader.join(); - col = darkenIfEquipped && isEquipped() ? ARGB.setBrightness(-1, 0.4f) : -1; classic = skin.isClassicVariant(); } else { - col = ARGB.setBrightness(-1, 0.6f); var skin = DefaultPlayerSkin.get(UUIDHelper.fromUndashed(owner.getUuid())); classic = skin.model() == PlayerSkin.Model.WIDE; skinRl = skin.texture(); } var capeRl = cape == null ? null : skinManager.loadCape(cape); + if (highlightIfEquipped && equipped) { + GradientHoleRectangleRenderState.create(guiGraphics, getX()-1, getY()-4, getRight()+1, getBottom(), getWidth() / 6, ClientColors.SELECTOR_GREEN.toInt(), 0).submit(); + } var renderer = SkinRenderer.getOrCreate(minecraft.renderBuffers().bufferSource(), minecraft, "" + hashCode()); ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState() .submitPicturesInPictureState( - new SkinRenderState(classic, (ResourceLocation) skinRl, (ResourceLocation) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getRight(), this.getBottom(), scale, guiGraphics.scissorStack.peek(), renderer, col)); + new SkinRenderState(classic, (ResourceLocation) skinRl, (ResourceLocation) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getRight(), this.getBottom(), scale, guiGraphics.scissorStack.peek(), renderer, -1)); } @Override @@ -102,6 +111,56 @@ protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) this.rotationY += (float) dragX * ROTATION_SENSITIVITY; } + private record GradientHoleRectangleRenderState(RenderPipeline pipeline, TextureSetup textureSetup, Matrix3x2f pose, + int x0, int y0, int x1, int y1, int gradientWidth, int col1, + int col2, @Nullable ScreenRectangle scissorArea, + @Nullable ScreenRectangle bounds) implements GuiElementRenderState { + + public static GradientHoleRectangleRenderState create(GuiGraphics graphics, int x0, int y0, int x1, int y1, int gradientWidth, int col1, int col2) { + var matrix = new Matrix3x2f(graphics.pose()); + var area = ((GuiGraphicsAccessor) graphics).getScissorStack().peek(); + return new GradientHoleRectangleRenderState(RenderPipelines.GUI, TextureSetup.noTexture(), matrix, x0, y0, x1, y1, gradientWidth, col1, col2, area, getBounds(x0, y0, x1, y1, matrix, area)); + } + + public void submit() { + ((GameRendererAccessor) Minecraft.getInstance().gameRenderer).getGuiRenderState().submitGuiElement(this); + } + + @Override + public void buildVertices(VertexConsumer vertexConsumer, float f) { + { //top + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); + } + { //left + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); + } + { //bottom + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); + } + { //right + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); + } + } + + @Nullable + private static ScreenRectangle getBounds(int i, int j, int k, int l, Matrix3x2f matrix3x2f, @Nullable ScreenRectangle screenRectangle) { + ScreenRectangle screenRectangle2 = new ScreenRectangle(i, j, k - i, l - j).transformMaxBounds(matrix3x2f); + return screenRectangle != null ? screenRectangle.intersection(screenRectangle2) : screenRectangle2; + } + } + @Override public void playDownSound(SoundManager handler) { } @@ -122,7 +181,7 @@ public ComponentPath nextFocusPath(FocusNavigationEvent event) { } public boolean isEquipped() { - return noCape ? noCapeActive : (cape != null ? cape.isActive() : skin == null || skin.isActive()); + return noCape ? noCapeActive : (cape != null ? cape.isActive() : skin != null && skin.isActive()); } public CompletableFuture equip() { diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index 6f49cf8d4..a2bbbf3d6 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -819,5 +819,6 @@ "skins.error.failed_to_load_desc": "Your log file may include more information.", "skins.capes.no_cape": "No Cape", "skins.manage.equipped": "Equipped", - "skins.manage.equip": "Equip" + "skins.manage.equip": "Equip", + "skins.loading": "Loading Skins..." } diff --git a/gradle.properties b/gradle.properties index ab4f34cc5..ec00b1a87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ fabric.loom.disableMinecraftVerification=true axolotlclient.modules.all=true # Mod Properties -version=3.1.6-alpha.1 +version=3.1.6-alpha.1+skins maven_group=io.github.axolotlclient From b169c4dd38bd6d5de0d6630df4ca59cc5a7539c3 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 30 Aug 2025 00:56:36 +0200 Subject: [PATCH 04/23] add more functionality, clean up a few things --- 1.16_combat-6/build.gradle.kts | 1 + 1.20/build.gradle.kts | 1 + 1.21.7/build.gradle.kts | 6 + .../axolotlclient/mixin/GuiRendererMixin.java | 22 ++ .../modules/auth/AccountsScreen.java | 7 +- .../modules/auth/AddOfflineScreen.java | 3 +- .../auth/skin/SkinManagementScreen.java | 280 +++++++++++++++--- .../modules/auth/skin/SkinManager.java | 24 +- .../modules/auth/skin/SkinRenderState.java | 22 ++ .../modules/auth/skin/SkinRenderer.java | 22 ++ .../modules/auth/skin/SkinWidget.java | 95 ++---- .../util/IdentifiablePiPRenderState.java | 22 ++ 1.21/build.gradle.kts | 1 + .../modules/auth/AddOfflineScreen.java | 1 - 1.8.9/build.gradle.kts | 1 + common/build.gradle.kts | 2 +- .../axolotlclient/modules/auth/Account.java | 1 + .../axolotlclient/modules/auth/MSApi.java | 13 +- .../modules/auth/skin/Asset.java | 22 ++ .../axolotlclient/modules/auth/skin/Cape.java | 22 ++ .../axolotlclient/modules/auth/skin/Skin.java | 22 ++ .../assets/axolotlclient/lang/en_us.json | 6 +- .../textures/gui/sprites/delete.png | Bin 0 -> 205 bytes 23 files changed, 465 insertions(+), 131 deletions(-) create mode 100644 common/src/main/resources/assets/axolotlclient/textures/gui/sprites/delete.png diff --git a/1.16_combat-6/build.gradle.kts b/1.16_combat-6/build.gradle.kts index 3dac0cf7d..acdabf07b 100644 --- a/1.16_combat-6/build.gradle.kts +++ b/1.16_combat-6/build.gradle.kts @@ -73,6 +73,7 @@ tasks.processResources { tasks.runClient { classpath(sourceSets.getByName("test").runtimeClasspath) + jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+IgnoreUnrecognizedVMOptions") } tasks.withType(JavaCompile::class).configureEach { diff --git a/1.20/build.gradle.kts b/1.20/build.gradle.kts index 852d41d34..a2dc7012f 100644 --- a/1.20/build.gradle.kts +++ b/1.20/build.gradle.kts @@ -87,6 +87,7 @@ java { tasks.runClient { classpath(sourceSets.getByName("test").runtimeClasspath) + jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+IgnoreUnrecognizedVMOptions") } // Configure the maven publication diff --git a/1.21.7/build.gradle.kts b/1.21.7/build.gradle.kts index 202d4c3c7..49f02d8a1 100644 --- a/1.21.7/build.gradle.kts +++ b/1.21.7/build.gradle.kts @@ -25,6 +25,11 @@ loom { sourceSet("test") } } + /*runs { + getByName("client") { + vmArg("-XX:+AllowEnhancedClassRedefinition -XX:+IgnoreUnrecognizedVMOptions") + } + }*/ } repositories { @@ -104,6 +109,7 @@ java { tasks.runClient { classpath(sourceSets.getByName("test").runtimeClasspath) + jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+IgnoreUnrecognizedVMOptions") } // Configure the maven publication diff --git a/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java index 72f8b4f28..2ee466417 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/mixin/GuiRendererMixin.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.mixin; import java.util.Map; diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 578cbf949..70ca489a6 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -80,8 +80,8 @@ public void init() { .bounds(this.width / 2 - 154, this.height - 52, 100, 20).build()); addRenderableWidget(skinsButton = Button.builder(Component.translatable("skins.manage"), - btn -> minecraft.setScreen(new SkinManagementScreen( - this, accountsListWidget.getSelected().getAccount()))) + btn -> minecraft.setScreen(new SkinManagementScreen( + this, accountsListWidget.getSelected().getAccount()))) .bounds(this.width / 2 - 50, this.height - 52, 100, 20).build()); this.addRenderableWidget(Button.builder(Component.translatable("auth.add"), button -> { @@ -141,7 +141,8 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (minecraft.level == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = skinsButton.active = true; + refreshButton.active = skinsButton.active = !entry.getAccount().isOffline(); + deleteButton.active = true; } else { loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java index fb42fbe32..60f9c5434 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java @@ -43,12 +43,11 @@ public AddOfflineScreen(Screen parent) { @Override public void render(GuiGraphics graphics, int i, int j, float f) { - renderBackground(graphics, i, j, f); super.render(graphics, i, j, f); graphics.drawString(font, Component.translatable("auth.add.offline.name"), (int) (width / 2F - 100), (int) (height / 2f - 20), -1 ); - graphics.drawString(this.font, this.title, this.width / 2, 20, -1); + graphics.drawCenteredString(this.font, this.title, this.width / 2, 20, -1); } @Override diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 813690c10..501579c84 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.io.IOException; @@ -8,12 +30,19 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.vertex.VertexConsumer; import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.mixin.GameRendererAccessor; +import io.github.axolotlclient.mixin.GuiGraphicsAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.ClientColors; import io.github.axolotlclient.util.Watcher; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; @@ -23,16 +52,25 @@ import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.render.TextureSetup; +import net.minecraft.client.gui.render.state.GuiElementRenderState; +import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.joml.Matrix3x2f; public class SkinManagementScreen extends Screen { private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); private static final int LIST_SKIN_WIDTH = 75; private static final int LIST_SKIN_HEIGHT = 110; + private static final MutableComponent TEXT_EQUIPPING = Component.translatable("skins.manage.equipping"); private final Screen parent; private final HeaderAndFooterLayout haF = new HeaderAndFooterLayout(this); private boolean initialized; @@ -65,7 +103,7 @@ protected void init() { haF.getHeaderHeight()); addRenderableWidget(loadingPlaceholder); skinList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, LIST_SKIN_HEIGHT + 34); - capesList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, skinList.getEntryContentsHeight() + 20); + capesList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); capesList.setX(width / 2); var currentHeight = Math.min((width / 2f) * 120 / 85, haF.getContentHeight()); @@ -121,8 +159,8 @@ protected void init() { } else { fut = CompletableFuture.completedFuture(null); } - fut.thenCompose(unused -> Auth.getInstance().getMsApi().getProfile(account)) - .thenAccept(profile -> { + fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAcceptAsync(profile -> { cachedProfile = profile; initDisplay(); addWidgets.run(); @@ -130,6 +168,8 @@ protected void init() { AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = Component.translatable("skins.error.failed_to_load"); var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); + removeWidget(loadingPlaceholder); + haF.visitWidgets(this::addRenderableWidget); addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(errorDesc), getFont().lineHeight, errorDesc, getFont())); return null; @@ -141,6 +181,18 @@ private void initDisplay() { loadCapesList(); } + private void refreshCurrentList() { + if (capesTab) { + var scroll = capesList.scrollAmount(); + loadCapesList(); + capesList.setScrollAmount(scroll); + } else { + var scroll = skinList.scrollAmount(); + loadSkinsList(); + skinList.setScrollAmount(scroll); + } + } + private void loadCapesList() { capesList.clearEntries(); var profile = cachedProfile; @@ -178,12 +230,12 @@ private void loadSkinsList() { List skins = new ArrayList<>(profile.skins()); var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); - if (!hashes.contains(defaultSkinHash)) { - skins.add(null); - } var local = new ArrayList<>(loadLocalSkins()); local.removeIf(s -> hashes.contains(s.textureKey())); skins.addAll(local); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } populateSkinList(skins, columns); } @@ -241,7 +293,7 @@ public void onFilesDrop(List packs) { } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { - return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account).highlightIfEquipped()); + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); } private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { @@ -249,7 +301,7 @@ public void onFilesDrop(List packs) { } private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { - SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account).highlightIfEquipped(); + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account); widget2.setRotationY(210); return widget2; } @@ -273,6 +325,10 @@ public void onClose() { minecraft.setScreen(parent); } + private SkinListWidget getCurrentList() { + return capesTab ? capesList : skinList; + } + private static class SkinListWidget extends ContainerObjectSelectionList { public SkinListWidget(Minecraft minecraft, int width, int height, int y, int entryHeight) { super(minecraft, width, height, y, entryHeight); @@ -315,9 +371,14 @@ public int getEntryContentsHeight() { public void clearEntries() { super.clearEntries(); } + + @Override + public void centerScrollOn(Row entry) { + super.centerScrollOn(entry); + } } - private static class Row extends ContainerObjectSelectionList.Entry { + private class Row extends ContainerObjectSelectionList.Entry { private final List widgets; public Row(List entries) { @@ -347,6 +408,14 @@ public void render(GuiGraphics guiGraphics, int index, int top, int left, int wi public @NotNull List children() { return widgets; } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + super.setFocused(focused); + if (focused != null) { + getCurrentList().centerScrollOn(this); + } + } } Entry createEntry(int height, SkinWidget widget) { @@ -354,42 +423,73 @@ Entry createEntry(int height, SkinWidget widget) { } Entry createEntry(int height, SkinWidget widget, Component label) { - List widgets = new ArrayList<>(label == null ? 2 : 3); - widgets.add(widget); - if (label != null) { - widgets.add(new AbstractStringWidget(0, 0, widget.getWidth(), 12, label, Minecraft.getInstance().font) { - @Override - protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - renderScrollingString(guiGraphics, getFont(), 2, -1); - } - }); - } - Button equip = Button.builder(Component.translatable( - widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), - btn -> widget.equip().thenAccept(p -> { - this.cachedProfile = p; - initDisplay(); - }).exceptionally(t -> { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); - return null; - })).width(widget.getWidth()).build(); - equip.active = !widget.isEquipped(); - widgets.add(equip); - return new Entry(widget.getWidth(), height, widgets); + return new Entry(height, widget, label); } - private static class Entry extends AbstractContainerWidget { - - private final List widgets; - - private Entry(int width, int height, List widgets) { - super(0, 0, width, height, Component.empty()); - this.widgets = widgets; + private class Entry extends AbstractContainerWidget { + private final SkinWidget skinWidget; + private final @Nullable AbstractWidget label; + private final @Nullable AbstractWidget trashButton; + private final AbstractWidget equipButton; + private boolean equipping; + private long equippingStart; + + public Entry(int height, SkinWidget widget, @Nullable Component label) { + super(0, 0, widget.getWidth(), height, Component.empty()); + widget.setWidth(getWidth() - 4); + if (widget.getSkin() instanceof Skin.Local local) { + this.trashButton = SpriteIconButton.builder(Component.translatable("skins.manage.delete"), btn -> { + btn.active = false; + minecraft.setScreen(new ConfirmScreen(confirmed -> { + minecraft.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(local.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete skin: ", e); + } + } + btn.active = true; + }, Component.translatable("skins.manage.delete.confirm"), Component.translatable("skins.manage.delete.confirm.desc") + .withColor(Colors.RED.toInt()))); + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11).build(); + } else { + trashButton = null; + } + if (label != null) { + this.label = new AbstractStringWidget(0, 0, widget.getWidth(), 16, label, Minecraft.getInstance().font) { + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + renderScrollingString(guiGraphics, getFont(), 2, -1); + } + }; + this.label.active = false; + } else { + this.label = null; + } + this.equipButton = Button.builder(Component.translatable( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> { + equippingStart = Util.getMillis(); + equipping = true; + btn.setMessage(TEXT_EQUIPPING); + btn.active = false; + widget.equip().thenAcceptAsync(p -> { + cachedProfile = p; + refreshCurrentList(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + return null; + }); + }).width(widget.getWidth()).build(); + this.equipButton.active = !widget.isEquipped(); + this.skinWidget = widget; } @Override public @NotNull List children() { - return widgets; + return Stream.of(trashButton, skinWidget, label, equipButton).filter(Objects::nonNull).toList(); } @Override @@ -404,13 +504,47 @@ protected double scrollRate() { @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - int y = getY()+4; - for (var w : widgets) { - w.setPosition(getX() + 2, y); - w.setWidth(getWidth() - 4); - w.render(guiGraphics, mouseX, mouseY, partialTick); - y += w.getHeight() + 4; + int y = getY() + 4; + int x = getX() + 2; + if (skinWidget.isEquipped() || equipping) { + long prog; + if (equipping) { + prog = (Util.getMillis() - equippingStart) / 20 % 100; + } else { + prog = Math.abs((Util.getMillis() / 50 % 100) - 50); + } + var percent = prog / 100f; + float gradientWidth; + if (equipping) { + gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); + } else { + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + percent * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + } + GradientHoleRectangleRenderState.create(guiGraphics, getX() + 2, getY() + 2, getRight() - 2, + skinWidget.getBottom() + 2, + gradientWidth, + equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0).submit(); + } + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); + skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); + if (trashButton != null) { + trashButton.setPosition(skinWidget.getRight() - trashButton.getWidth(), getY() + 2); + if (isHovered() || trashButton.isHoveredOrFocused()) { + trashButton.render(guiGraphics, mouseX, mouseY, partialTick); + } + } + if (label != null) { + label.setPosition(x, skinWidget.getBottom() + 6); + label.render(guiGraphics, mouseX, mouseY, partialTick); + label.setWidth(getWidth() - 4); + equipButton.setPosition(x, label.getBottom() + 2); + } else { + equipButton.setPosition(x, skinWidget.getBottom() + 4); } + equipButton.setWidth(getWidth() - 4); + equipButton.render(guiGraphics, mouseX, mouseY, partialTick); + if (isHovered()) { guiGraphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); } @@ -418,7 +552,61 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo @Override protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { - widgets.forEach(w -> w.updateNarration(narrationElementOutput)); + skinWidget.updateNarration(narrationElementOutput); + if (trashButton != null) { + trashButton.updateNarration(narrationElementOutput); + } + if (label != null) { + label.updateNarration(narrationElementOutput); + } + equipButton.updateNarration(narrationElementOutput); + } + + private record GradientHoleRectangleRenderState(RenderPipeline pipeline, TextureSetup textureSetup, + Matrix3x2f pose, + int x0, int y0, int x1, int y1, float gradientWidth, int col1, + int col2, @Nullable ScreenRectangle scissorArea, + @Nullable ScreenRectangle bounds) implements GuiElementRenderState { + + public static GradientHoleRectangleRenderState create(GuiGraphics graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { + var matrix = new Matrix3x2f(graphics.pose()); + var area = ((GuiGraphicsAccessor) graphics).getScissorStack().peek(); + return new GradientHoleRectangleRenderState(RenderPipelines.GUI, TextureSetup.noTexture(), matrix, x0, y0, x1, y1, gradientWidth, col1, col2, area, getBounds(x0, y0, x1, y1, matrix, area)); + } + + public void submit() { + ((GameRendererAccessor) Minecraft.getInstance().gameRenderer).getGuiRenderState().submitGuiElement(this); + } + + @Override + public void buildVertices(VertexConsumer vertexConsumer, float f) { + //top + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); + //left + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); + //bottom + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); + //right + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); + } + + @Nullable + private static ScreenRectangle getBounds(int i, int j, int k, int l, Matrix3x2f matrix3x2f, @Nullable ScreenRectangle screenRectangle) { + ScreenRectangle screenRectangle2 = new ScreenRectangle(i, j, k - i, l - j).transformMaxBounds(matrix3x2f); + return screenRectangle != null ? screenRectangle.intersection(screenRectangle2) : screenRectangle2; + } } } } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 3d9a16c7b..40f55af56 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.io.IOException; @@ -44,7 +66,7 @@ public Skin read(Path p) { } - public CompletableFuture loadSkin(Skin skin, Account owner) { + public CompletableFuture loadSkin(Skin skin) { var rl = getRl(skin); if (loadedTextures.contains(rl)) { return CompletableFuture.completedFuture(rl); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java index fd154ce16..a1f66bebb 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import io.github.axolotlclient.util.IdentifiablePiPRenderState; diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 5281c0d44..595590dd6 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.util.Map; diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index b1d4ea5d8..4758df067 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -1,17 +1,35 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.util.concurrent.CompletableFuture; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.vertex.VertexConsumer; import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.util.AxoIdentifier; -import io.github.axolotlclient.mixin.GameRendererAccessor; import io.github.axolotlclient.mixin.GuiGraphicsAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; -import io.github.axolotlclient.util.ClientColors; import lombok.Getter; import lombok.Setter; import net.minecraft.client.Minecraft; @@ -20,10 +38,6 @@ import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; -import net.minecraft.client.gui.navigation.ScreenRectangle; -import net.minecraft.client.gui.render.TextureSetup; -import net.minecraft.client.gui.render.state.GuiElementRenderState; -import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.client.resources.PlayerSkin; import net.minecraft.client.sounds.SoundManager; @@ -31,7 +45,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; -import org.joml.Matrix3x2f; public class SkinWidget extends AbstractWidget { private static final float MODEL_HEIGHT = 2.125F; @@ -51,7 +64,6 @@ public class SkinWidget extends AbstractWidget { private Cape cape; private final Account owner; private boolean noCape, noCapeActive; - private boolean highlightIfEquipped; public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { super(0, 0, width, height, CommonComponents.EMPTY); @@ -69,11 +81,6 @@ public void noCape(boolean noCapeActive) { this.noCapeActive = noCapeActive; } - public SkinWidget highlightIfEquipped() { - highlightIfEquipped = true; - return this; - } - @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { var minecraft = Minecraft.getInstance(); @@ -83,9 +90,8 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo AxoIdentifier skinRl; boolean classic; - boolean equipped = isEquipped(); SkinManager skinManager = Auth.getInstance().getSkinManager(); - CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin, owner); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); classic = skin.isClassicVariant(); @@ -95,10 +101,9 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo skinRl = skin.texture(); } var capeRl = cape == null ? null : skinManager.loadCape(cape); - if (highlightIfEquipped && equipped) { - GradientHoleRectangleRenderState.create(guiGraphics, getX()-1, getY()-4, getRight()+1, getBottom(), getWidth() / 6, ClientColors.SELECTOR_GREEN.toInt(), 0).submit(); - } + // You might say that using `hashCode()` like this isn't ideal, but in reality it doesn't matter. These objects get freed + // correctly by the screen so we mostly only need unique identifiers per widget which `hashCode()` provides. var renderer = SkinRenderer.getOrCreate(minecraft.renderBuffers().bufferSource(), minecraft, "" + hashCode()); ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState() .submitPicturesInPictureState( @@ -111,56 +116,6 @@ protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) this.rotationY += (float) dragX * ROTATION_SENSITIVITY; } - private record GradientHoleRectangleRenderState(RenderPipeline pipeline, TextureSetup textureSetup, Matrix3x2f pose, - int x0, int y0, int x1, int y1, int gradientWidth, int col1, - int col2, @Nullable ScreenRectangle scissorArea, - @Nullable ScreenRectangle bounds) implements GuiElementRenderState { - - public static GradientHoleRectangleRenderState create(GuiGraphics graphics, int x0, int y0, int x1, int y1, int gradientWidth, int col1, int col2) { - var matrix = new Matrix3x2f(graphics.pose()); - var area = ((GuiGraphicsAccessor) graphics).getScissorStack().peek(); - return new GradientHoleRectangleRenderState(RenderPipelines.GUI, TextureSetup.noTexture(), matrix, x0, y0, x1, y1, gradientWidth, col1, col2, area, getBounds(x0, y0, x1, y1, matrix, area)); - } - - public void submit() { - ((GameRendererAccessor) Minecraft.getInstance().gameRenderer).getGuiRenderState().submitGuiElement(this); - } - - @Override - public void buildVertices(VertexConsumer vertexConsumer, float f) { - { //top - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); - } - { //left - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); - } - { //bottom - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); - } - { //right - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); - } - } - - @Nullable - private static ScreenRectangle getBounds(int i, int j, int k, int l, Matrix3x2f matrix3x2f, @Nullable ScreenRectangle screenRectangle) { - ScreenRectangle screenRectangle2 = new ScreenRectangle(i, j, k - i, l - j).transformMaxBounds(matrix3x2f); - return screenRectangle != null ? screenRectangle.intersection(screenRectangle2) : screenRectangle2; - } - } - @Override public void playDownSound(SoundManager handler) { } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java index 905881a62..574e776b6 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/util/IdentifiablePiPRenderState.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.util; import net.minecraft.client.gui.render.pip.PictureInPictureRenderer; diff --git a/1.21/build.gradle.kts b/1.21/build.gradle.kts index 271d5ac54..3ba61ec8e 100644 --- a/1.21/build.gradle.kts +++ b/1.21/build.gradle.kts @@ -89,6 +89,7 @@ java { tasks.runClient { classpath(sourceSets.getByName("test").runtimeClasspath) + jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+IgnoreUnrecognizedVMOptions") } // Configure the maven publication diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java index 9da89256e..528f8e8df 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AddOfflineScreen.java @@ -43,7 +43,6 @@ public AddOfflineScreen(Screen parent) { @Override public void render(GuiGraphics graphics, int i, int j, float f) { - renderBackground(graphics, i, j, f); super.render(graphics, i, j, f); graphics.drawShadowedText(textRenderer, Text.translatable("auth.add.offline.name"), (int) (width / 2F - 100), (int) (height / 2f - 20), -1); graphics.drawCenteredShadowedText(this.textRenderer, this.title, this.width / 2, 20, 16777215); diff --git a/1.8.9/build.gradle.kts b/1.8.9/build.gradle.kts index e3045c94e..02e6869d5 100644 --- a/1.8.9/build.gradle.kts +++ b/1.8.9/build.gradle.kts @@ -102,6 +102,7 @@ tasks.runClient { jvmArgs("-Dorg.lwjgl.glfw.libname=$glfwPath") } classpath(sourceSets.getByName("test").runtimeClasspath) + jvmArgs("-XX:+AllowEnhancedClassRedefinition", "-XX:+IgnoreUnrecognizedVMOptions") } tasks.withType(JavaCompile::class).configureEach { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 85cd9d7e0..3cd33558a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { shadow(implementation("com.kohlschutter.junixsocket:junixsocket-common:2.10.1")!!) shadow(implementation("com.kohlschutter.junixsocket:junixsocket-native-common:2.10.1")!!) - shadow(implementation("com.github.mizosoft.methanol:methanol:1.8.0")!!) + shadow(implementation("com.github.mizosoft.methanol:methanol:1.8.3")!!) shadow(implementation("io.nayuki:qrcodegen:1.8.0")!!) compileOnly("net.hypixel:mod-api:1.0.1") diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java index fdfb417f3..31e070420 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java @@ -84,6 +84,7 @@ public static Account deserialize(JsonObject object) { } public CompletableFuture> refresh(MSApi auth) { + if (isOffline()) return CompletableFuture.completedFuture(Optional.empty()); return auth.refreshToken(refreshToken, this); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index 73f493be2..98a440411 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -84,7 +84,7 @@ public CompletableFuture startDeviceAuth() { .POST(FormBodyPublisher.newBuilder() .query("client_id", CLIENT_ID) .query("scope", SCOPES).build()) - .header("ContentType", "application/x-www-form-urlencoded") + .header("content-type", "application/x-www-form-urlencoded") .uri(URI.create(MS_DEVICE_CODE_LOGIN_URL + lang[0] + "-" + lang[1].toUpperCase(Locale.ROOT))); return requestJson(builder.build()) .thenApply(object -> { @@ -272,8 +272,8 @@ private CompletableFuture authXbl(String code) { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(XBL_AUTH_URL)) .POST(HttpRequest.BodyPublishers.ofString(object.toString())) - .header("Content-Type", "application/json") - .header("Accept", "application/json"); + .header("content-type", "application/json") + .header("accept", "application/json"); return requestJson(requestBuilder.build()).thenApply(response -> new XblData(Instant.parse(response.get("IssueInstant").getAsString()), Instant.parse(response.get("NotAfter").getAsString()), response.get("Token").getAsString(), new XblData.DisplayClaims(response.get("DisplayClaims").getAsJsonObject().get("xui").getAsJsonArray().get(0).getAsJsonObject().get("uhs").getAsString()))); @@ -379,6 +379,10 @@ private MCProfile extractProfile(JsonObject profileJson) { if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { throw new IllegalStateException("profile not found"); } + if (!profileJson.has("id")) { + logger.warn("Unexpected profile response: {}", profileJson); + throw new IllegalStateException("unexpected error"); + } return MCProfile.get(profileJson); } @@ -419,9 +423,6 @@ public CompletableFuture hideCape(Account account) { .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/active")) .header("Authorization", "Bearer " + account.getAuthToken()) .build()) - .thenApply(js -> { - return js; - }) .thenApply(this::extractProfile); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java index 35f62e964..1a02c91e0 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.util.concurrent.CompletableFuture; diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java index 0275f0fcb..11ba1a670 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Cape.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; public interface Cape extends Asset { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index 3478a5bac..012b62213 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.modules.auth.skin; import java.io.IOException; diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index a2bbbf3d6..de41b1396 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -820,5 +820,9 @@ "skins.capes.no_cape": "No Cape", "skins.manage.equipped": "Equipped", "skins.manage.equip": "Equip", - "skins.loading": "Loading Skins..." + "skins.loading": "Loading Skins...", + "skins.manage.equipping": "Equipping...", + "skins.manage.delete": "Delete Skin", + "skins.manage.delete.confirm": "Confirm Deletion", + "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure?" } diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/delete.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..cd0b75860754d1f0e3caa0e1da8ed9916e28dc22 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJgeK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a z5n0T@pr;JNj1^1m%YcIHC7!;n>@PXw#8`!1`&{$@3MqTKIEHXsPyTWKz=!#a+zs7} qwGUii5I&G9l*l>9O{j!t6BC1*kmU2#oq_^D Date: Sun, 31 Aug 2025 12:49:01 +0200 Subject: [PATCH 05/23] add download action button --- .../axolotlclient/modules/auth/Auth.java | 2 - .../axolotlclient/modules/auth/Auth.java | 2 - .../axolotlclient/modules/auth/Auth.java | 5 +- .../auth/skin/SkinManagementScreen.java | 111 ++++++++++++------ .../modules/auth/skin/SkinManager.java | 10 +- .../modules/auth/skin/SkinWidget.java | 6 +- .../axolotlclient/modules/auth/Auth.java | 2 - .../axolotlclient/modules/auth/Auth.java | 2 - .../axolotlclient/modules/auth/Accounts.java | 3 + .../axolotlclient/modules/auth/MSApi.java | 24 +++- .../modules/auth/skin/Asset.java | 26 +++- .../axolotlclient/modules/auth/skin/Skin.java | 67 ++++++++++- .../assets/axolotlclient/lang/en_us.json | 4 +- .../textures/gui/sprites/download.png | Bin 0 -> 219 bytes 14 files changed, 200 insertions(+), 64 deletions(-) create mode 100644 common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index f050ffd26..9d985180d 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -27,7 +27,6 @@ import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; import io.github.axolotlclient.AxolotlClient; -import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; import io.github.axolotlclient.api.API; import io.github.axolotlclient.api.types.User; @@ -73,7 +72,6 @@ public void init() { current = new Account(client.getSession().getUsername(), client.getSession().getUuid(), client.getSession().getAccessToken()); } - OptionCategory category = OptionCategory.create("auth"); category.add(showButton, viewAccounts); AxolotlClient.config().general.add(category); } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index bb5719748..00c023829 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -28,7 +28,6 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.UserApiService; import io.github.axolotlclient.AxolotlClient; -import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; import io.github.axolotlclient.api.API; import io.github.axolotlclient.api.types.User; @@ -77,7 +76,6 @@ public void init() { current = new Account(client.getSession().getUsername(), UUIDHelper.toUndashed(client.getSession().getPlayerUuid()), client.getSession().getAccessToken()); } - OptionCategory category = OptionCategory.create("auth"); category.add(showButton, viewAccounts); AxolotlClient.config().general.add(category); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 93f7992e0..8d2658761 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -28,7 +28,6 @@ import com.mojang.authlib.minecraft.UserApiService; import com.mojang.authlib.yggdrasil.ProfileResult; import io.github.axolotlclient.AxolotlClient; -import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; import io.github.axolotlclient.api.API; import io.github.axolotlclient.api.util.UUIDHelper; @@ -60,6 +59,7 @@ public class Auth extends Accounts implements Module { @Getter private final static Auth Instance = new Auth(); public final BooleanOption showButton = new BooleanOption("auth.showButton", false); + public final BooleanOption skinManagerAnimations = new BooleanOption("skins.manage.animations", true); private final Minecraft mc = Minecraft.getInstance(); private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> mc.setScreen(new AccountsScreen(mc.screen))); private final Set loadingTexture = new HashSet<>(); @@ -80,8 +80,7 @@ public void init() { current = new Account(mc.getUser().getName(), UUIDHelper.toUndashed(mc.getUser().getProfileId()), mc.getUser().getAccessToken()); } - OptionCategory category = OptionCategory.create("auth"); - category.add(showButton, viewAccounts); + category.add(showButton, viewAccounts, skinManagerAnimations); AxolotlClient.config().general.add(category); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 501579c84..db44375cd 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -27,6 +27,7 @@ import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -98,6 +99,7 @@ protected void init() { haF.addToFooter(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()).build()); } haF.arrangeElements(); + haF.visitWidgets(this::addRenderableWidget); var loadingPlaceholder = new LoadingDotsWidget(getFont(), Component.translatable("skins.loading")); loadingPlaceholder.setRectangle(width, haF.getContentHeight(), 0, haF.getHeaderHeight()); @@ -140,6 +142,7 @@ protected void init() { skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { + haF.visitWidgets(this::removeWidget); removeWidget(loadingPlaceholder); addRenderableWidget(current); addRenderableWidget(skinsTab); @@ -169,7 +172,6 @@ protected void init() { var error = Component.translatable("skins.error.failed_to_load"); var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); removeWidget(loadingPlaceholder); - haF.visitWidgets(this::addRenderableWidget); addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(errorDesc), getFont().lineHeight, errorDesc, getFont())); return null; @@ -199,7 +201,7 @@ private void loadCapesList() { int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); var capes = profile.capes(); var deselectCape = createWidgetForCape(current.getSkin(), null); - var activeCape = capes.stream().filter(Cape::isActive).findFirst(); + var activeCape = capes.stream().filter(Cape::active).findFirst(); current.setCape(activeCape.orElse(null)); deselectCape.noCape(activeCape.isEmpty()); for (int i = 0; i < capes.size() + 1; i += columns) { @@ -231,7 +233,17 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - local.removeIf(s -> hashes.contains(s.textureKey())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + skins.replaceAll(s -> { + if (s instanceof MSApi.MCProfile.OnlineSkin online) { + if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { + local.remove(localHashes.remove(s.textureKey())); + return new Skin.Shared(file, online); + } + } + return s; + }); + //local.removeIf(s -> hashes.contains(s.textureKey())); skins.addAll(local); if (!hashes.contains(defaultSkinHash)) { skins.add(null); @@ -261,7 +273,7 @@ private void populateSkinList(List skins, int columns) { int entryHeight = skinList.getEntryContentsHeight(); for (int i = 0; i < skins.size(); i += columns) { var s = skins.get(i); - if (s != null && s.isActive()) { + if (s != null && s.active()) { current.setSkin(s); } var widget = createEntryForSkin(s, entryHeight); @@ -270,7 +282,7 @@ private void populateSkinList(List skins, int columns) { for (int c = 1; c < columns; c++) { if (!(i < skins.size() - c)) continue; var s2 = skins.get(i + c); - if (s2 != null && s2.isActive()) { + if (s2 != null && s2.active()) { current.setSkin(s2); } var widget2 = createEntryForSkin(s2, entryHeight); @@ -429,7 +441,7 @@ Entry createEntry(int height, SkinWidget widget, Component label) { private class Entry extends AbstractContainerWidget { private final SkinWidget skinWidget; private final @Nullable AbstractWidget label; - private final @Nullable AbstractWidget trashButton; + private final List actionButtons = new ArrayList<>(); private final AbstractWidget equipButton; private boolean equipping; private long equippingStart; @@ -437,25 +449,47 @@ private class Entry extends AbstractContainerWidget { public Entry(int height, SkinWidget widget, @Nullable Component label) { super(0, 0, widget.getWidth(), height, Component.empty()); widget.setWidth(getWidth() - 4); - if (widget.getSkin() instanceof Skin.Local local) { - this.trashButton = SpriteIconButton.builder(Component.translatable("skins.manage.delete"), btn -> { - btn.active = false; - minecraft.setScreen(new ConfirmScreen(confirmed -> { - minecraft.setScreen(SkinManagementScreen.this); - if (confirmed) { + var asset = widget.getFocusedAsset(); + if (asset != null) { + if (asset.isLocal()) { + var delete = SpriteIconButton.builder(Component.translatable("skins.manage.delete"), btn -> { + btn.active = false; + minecraft.setScreen(new ConfirmScreen(confirmed -> { + minecraft.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); + } + } + btn.active = true; + }, Component.translatable("skins.manage.delete.confirm"), Component.translatable("skins.manage.delete.confirm.desc") + .withColor(Colors.RED.toInt()))); + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11) + .build(); + delete.setTooltip(Tooltip.create(delete.getMessage())); + this.actionButtons.add(delete); + } + if (asset.supportsDownload() && !asset.isLocal()) { + var download = SpriteIconButton.builder(Component.translatable("skins.manage.download"), btn -> { + btn.active = false; + asset.image().thenAcceptAsync(b -> { try { - Files.delete(local.file()); - refreshCurrentList(); + var out = SKINS_DIR.resolve(asset.textureKey()); + Files.createDirectories(out.getParent()); + Files.write(out, b); } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete skin: ", e); + AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } - } - btn.active = true; - }, Component.translatable("skins.manage.delete.confirm"), Component.translatable("skins.manage.delete.confirm.desc") - .withColor(Colors.RED.toInt()))); - }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11).build(); - } else { - trashButton = null; + refreshCurrentList(); + btn.active = true; + }); + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); + download.setTooltip(Tooltip.create(download.getMessage())); + this.actionButtons.add(download); + } } if (label != null) { this.label = new AbstractStringWidget(0, 0, widget.getWidth(), 16, label, Minecraft.getInstance().font) { @@ -489,7 +523,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo @Override public @NotNull List children() { - return Stream.of(trashButton, skinWidget, label, equipButton).filter(Objects::nonNull).toList(); + return Stream.concat(actionButtons.stream(), Stream.of(skinWidget, label, equipButton)).filter(Objects::nonNull).toList(); } @Override @@ -502,23 +536,26 @@ protected double scrollRate() { return 0; } + private float applyEasing(float x) { + return x * x * x; + } + @Override protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { int y = getY() + 4; int x = getX() + 2; if (skinWidget.isEquipped() || equipping) { long prog; - if (equipping) { - prog = (Util.getMillis() - equippingStart) / 20 % 100; - } else { - prog = Math.abs((Util.getMillis() / 50 % 100) - 50); - } - var percent = prog / 100f; + if (Auth.getInstance().skinManagerAnimations.get()) { + if (equipping) prog = (Util.getMillis() - equippingStart) / 20 % 100; + else prog = Math.abs((Util.getMillis() / 30 % 200) - 100); + } else prog = 100; + var percent = (prog / 100f); float gradientWidth; if (equipping) { gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); } else { - gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + percent * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + applyEasing(percent) * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); } GradientHoleRectangleRenderState.create(guiGraphics, getX() + 2, getY() + 2, getRight() - 2, skinWidget.getBottom() + 2, @@ -528,11 +565,13 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo skinWidget.setPosition(x, y); skinWidget.setWidth(getWidth() - 4); skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); - if (trashButton != null) { - trashButton.setPosition(skinWidget.getRight() - trashButton.getWidth(), getY() + 2); - if (isHovered() || trashButton.isHoveredOrFocused()) { - trashButton.render(guiGraphics, mouseX, mouseY, partialTick); + int actionButtonY = getY() + 2; + for (var button : actionButtons) { + button.setPosition(skinWidget.getRight() - button.getWidth(), actionButtonY); + if (isHovered() || button.isHoveredOrFocused()) { + button.render(guiGraphics, mouseX, mouseY, partialTick); } + actionButtonY += button.getHeight() + 2; } if (label != null) { label.setPosition(x, skinWidget.getBottom() + 6); @@ -553,9 +592,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo @Override protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { skinWidget.updateNarration(narrationElementOutput); - if (trashButton != null) { - trashButton.updateNarration(narrationElementOutput); - } + actionButtons.forEach(w -> w.updateNarration(narrationElementOutput)); if (label != null) { label.updateNarration(narrationElementOutput); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 40f55af56..b46e9d803 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -59,7 +59,7 @@ public Skin read(Path p) { slim = (ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0); } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); - } catch (IOException e) { + } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } return null; @@ -72,7 +72,7 @@ public CompletableFuture loadSkin(Skin skin) { return CompletableFuture.completedFuture(rl); } - return skin.getImage().thenApplyAsync(bytes -> { + return skin.image().thenApplyAsync(bytes -> { try { var tex = new DynamicTexture(rl::toString, NativeImage.read(bytes)); Minecraft.getInstance().getTextureManager().register((ResourceLocation) rl, tex); @@ -95,7 +95,7 @@ public AxoIdentifier loadCape(Cape cape) { return rl; } - return cape.getImage().thenApplyAsync(bytes -> { + return cape.image().thenApplyAsync(bytes -> { try { var tex = new DynamicTexture(rl::toString, NativeImage.read(bytes)); Minecraft.getInstance().getTextureManager().register((ResourceLocation) rl, tex); @@ -119,11 +119,11 @@ public void releaseAll() { } private @NotNull AxoIdentifier getRl(Skin skin) { - return AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + Hashing.sha256().hashUnencodedChars(skin.id())); + return AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); } private @NotNull AxoIdentifier getRl(Cape cape) { - return AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + Hashing.sha256().hashUnencodedChars(cape.id())); + return AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); } public String getDefaultSkinHash(Account account) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 4758df067..df4e68df2 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -136,7 +136,7 @@ public ComponentPath nextFocusPath(FocusNavigationEvent event) { } public boolean isEquipped() { - return noCape ? noCapeActive : (cape != null ? cape.isActive() : skin != null && skin.isActive()); + return noCape ? noCapeActive : (cape != null ? cape.active() : skin != null && skin.active()); } public CompletableFuture equip() { @@ -152,4 +152,8 @@ public CompletableFuture equip() { } return msApi.resetSkin(owner); } + + public Asset getFocusedAsset() { + return noCape ? null : cape != null ? cape : skin; + } } diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index bf4acb06c..726ea4fd3 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -27,7 +27,6 @@ import com.mojang.authlib.minecraft.UserApiService; import com.mojang.authlib.yggdrasil.ProfileResult; import io.github.axolotlclient.AxolotlClient; -import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; import io.github.axolotlclient.api.API; import io.github.axolotlclient.api.types.User; @@ -76,7 +75,6 @@ public void init() { current = new Account(client.getSession().getUsername(), UUIDHelper.toUndashed(client.getSession().getPlayerUuid()), client.getSession().getAccessToken()); } - OptionCategory category = OptionCategory.create("auth"); category.add(showButton, viewAccounts); AxolotlClient.config().general.add(category); } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 4ec787ec3..2624af180 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -27,7 +27,6 @@ import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; import io.github.axolotlclient.AxolotlClient; -import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.AxolotlClientConfig.impl.options.BooleanOption; import io.github.axolotlclient.api.API; import io.github.axolotlclient.api.types.User; @@ -74,7 +73,6 @@ public void init() { current = new Account(client.getSession().getUsername(), client.getSession().getUuid(), client.getSession().getAccessToken()); } - OptionCategory category = OptionCategory.create("auth"); category.add(showButton, viewAccounts); AxolotlClient.config().general.add(category); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java index 93e0ab0a8..a1adec819 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java @@ -31,6 +31,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.options.OptionCategory; import io.github.axolotlclient.util.GsonHelper; import io.github.axolotlclient.util.Logger; import lombok.Getter; @@ -38,6 +39,8 @@ @Getter public abstract class Accounts { + public final OptionCategory category = OptionCategory.create("auth"); + private final List accounts = new ArrayList<>(); protected Account current; protected MSApi msApi; diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index 98a440411..61f3b6b86 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -201,7 +201,12 @@ public static OnlineSkin get(JsonObject object) { url.substring(url.lastIndexOf("/")+1)); } - public CompletableFuture getImage() { + @Override + public boolean isOnline() { + return true; + } + + public CompletableFuture image() { return INSTANCE.client.sendAsync(HttpRequest.newBuilder(URI.create(url())).GET().build(), HttpResponse.BodyHandlers.ofByteArray()) .thenApplyAsync(res -> { if (res.statusCode() == 200) { @@ -219,7 +224,7 @@ public boolean isSlimVariant() { return VARIANT_SLIM.equals(variant()); } - public boolean isActive() { + public boolean active() { return STATE_ACTIVE.equals(state()); } @@ -227,6 +232,11 @@ public boolean isActive() { public CompletableFuture equip(MSApi api, Account account) { return api.setSkin(account, this); } + + @Override + public boolean supportsDownload() { + return true; + } } public record OnlineCape(String id, String state, String url, String alias, String textureKey) implements Cape { @@ -238,7 +248,7 @@ public static OnlineCape get(JsonObject object) { url, object.get("alias").getAsString(), url.substring(url.lastIndexOf("/")+1)); } - public CompletableFuture getImage() { + public CompletableFuture image() { return INSTANCE.client.sendAsync(HttpRequest.newBuilder(URI.create(url())).GET().build(), HttpResponse.BodyHandlers.ofByteArray()) .thenApplyAsync(res -> { if (res.statusCode() == 200) { @@ -248,7 +258,12 @@ public CompletableFuture getImage() { }); } - public boolean isActive() { + @Override + public boolean isOnline() { + return true; + } + + public boolean active() { return STATE_ACTIVE.equals(state()); } @@ -257,7 +272,6 @@ public CompletableFuture equip(MSApi api, Account account) { return api.showCape(account, this); } } - } private CompletableFuture authXbl(String code) { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java index 1a02c91e0..8a66a30de 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth.skin; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; import io.github.axolotlclient.modules.auth.Account; @@ -29,9 +30,30 @@ public interface Asset { String id(); - CompletableFuture getImage(); - boolean isActive(); + default boolean isOnline() { + return false; + } + + default boolean isLocal() { + return false; + } + + default Path file() { + return null; + } + + default String url() { + return null; + } + + default boolean supportsDownload() { + return false; + } + + CompletableFuture image(); + + boolean active(); CompletableFuture equip(MSApi api, Account account); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index 012b62213..6be110bea 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -42,7 +42,7 @@ public boolean isClassicVariant() { } @Override - public CompletableFuture getImage() { + public CompletableFuture image() { return CompletableFuture.supplyAsync(() -> { try { return Files.readAllBytes(file); @@ -53,7 +53,7 @@ public CompletableFuture getImage() { } @Override - public boolean isActive() { + public boolean active() { return false; } @@ -61,5 +61,68 @@ public boolean isActive() { public CompletableFuture equip(MSApi api, Account account) { return api.uploadAndSetSkin(account, this); } + + @Override + public boolean isLocal() { + return true; + } + } + + record Shared(Local local, MSApi.MCProfile.OnlineSkin online) implements Skin { + + @Override + public boolean isClassicVariant() { + return online.isClassicVariant(); + } + + @Override + public String id() { + return online.id(); + } + + @Override + public CompletableFuture image() { + return local.image(); + } + + @Override + public boolean active() { + return online.active(); + } + + @Override + public CompletableFuture equip(MSApi api, Account account) { + return online.equip(api, account); + } + + @Override + public String textureKey() { + return local.textureKey(); + } + + @Override + public boolean isOnline() { + return true; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public Path file() { + return local.file(); + } + + @Override + public String url() { + return online().url(); + } + + @Override + public boolean supportsDownload() { + return true; + } } } diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index de41b1396..653cb0168 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -824,5 +824,7 @@ "skins.manage.equipping": "Equipping...", "skins.manage.delete": "Delete Skin", "skins.manage.delete.confirm": "Confirm Deletion", - "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure?" + "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", + "skins.manage.animations": "Skin Manger Animations", + "skins.manage.download": "Download Skin" } diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png new file mode 100644 index 0000000000000000000000000000000000000000..84f7fb765d29c8941b67d6626d0e6ba8bf7665ae GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJgeK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a z5n0T@pr;JNj1^1m%YcIHC7!;n>@PXwIgR<(O;~>hC}imA;uyklJvqUFiJed8NB^1h zqyx+|HoR>3D7jJc<%0(YIudzyan4}!c+|5)MN&dSf?@3vsRIW+Cd>zFX7F_Nb6Mw< G&;$V4Up$)t literal 0 HcmV?d00001 From 205497824073371591ab13d84eb516471699cd89 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 31 Aug 2025 16:35:32 +0200 Subject: [PATCH 06/23] port to 1.21.1 --- .../auth/skin/SkinManagementScreen.java | 75 +-- .../modules/auth/skin/SkinManager.java | 13 +- .../modules/auth/skin/SkinRenderState.java | 8 +- .../modules/auth/skin/SkinRenderer.java | 5 +- .../modules/auth/skin/SkinWidget.java | 2 +- ...orizontalGradientRectangleRenderState.java | 10 +- .../modules/auth/AccountsScreen.java | 18 +- .../axolotlclient/modules/auth/Auth.java | 4 + .../auth/skin/SkinManagementScreen.java | 629 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 134 ++++ .../modules/auth/skin/SkinRenderer.java | 110 +++ .../modules/auth/skin/SkinWidget.java | 152 +++++ .../assets/axolotlclient/lang/en_us.json | 3 +- 13 files changed, 1094 insertions(+), 69 deletions(-) create mode 100644 1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java create mode 100644 1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java create mode 100644 1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java create mode 100644 1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index db44375cd..fba1d8b3f 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -49,7 +49,6 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.*; import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; @@ -61,7 +60,6 @@ import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -71,10 +69,8 @@ public class SkinManagementScreen extends Screen { private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); private static final int LIST_SKIN_WIDTH = 75; private static final int LIST_SKIN_HEIGHT = 110; - private static final MutableComponent TEXT_EQUIPPING = Component.translatable("skins.manage.equipping"); + private static final Component TEXT_EQUIPPING = Component.translatable("skins.manage.equipping"); private final Screen parent; - private final HeaderAndFooterLayout haF = new HeaderAndFooterLayout(this); - private boolean initialized; private final Account account; private MSApi.MCProfile cachedProfile; private SkinListWidget skinList; @@ -92,23 +88,22 @@ public SkinManagementScreen(Screen parent, Account account) { @Override protected void init() { - if (!initialized) { - initialized = true; + int headerHeight = 33; + int contentHeight = height - headerHeight * 2; + + addRenderableWidget(new StringWidget(0, headerHeight/2-font.lineHeight/2, width, font.lineHeight, getTitle(), getFont())); + var back = addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()) + .bounds(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); - haF.addTitleHeader(getTitle(), getFont()); - haF.addToFooter(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()).build()); - } - haF.arrangeElements(); - haF.visitWidgets(this::addRenderableWidget); var loadingPlaceholder = new LoadingDotsWidget(getFont(), Component.translatable("skins.loading")); - loadingPlaceholder.setRectangle(width, haF.getContentHeight(), 0, - haF.getHeaderHeight()); + loadingPlaceholder.setRectangle(width, contentHeight, 0, + headerHeight); addRenderableWidget(loadingPlaceholder); - skinList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, LIST_SKIN_HEIGHT + 34); - capesList = new SkinListWidget(minecraft, width / 2, haF.getContentHeight() - 24, haF.getHeaderHeight() + 24, skinList.getEntryContentsHeight() + 24); + skinList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); capesList.setX(width / 2); - var currentHeight = Math.min((width / 2f) * 120 / 85, haF.getContentHeight()); + var currentHeight = Math.min((width / 2f) * 120 / 85, contentHeight); var currentWidth = currentHeight * 85 / 120; current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); @@ -127,7 +122,7 @@ protected void init() { skinList.visible = skinList.active = true; capesList.visible = capesList.active = false; capesTab = false; - }).pos(width * 3 / 4 - 102, haF.getHeaderHeight()).width(100).build(); + }).pos(width * 3 / 4 - 102, headerHeight).width(100).build(); navBar.add(skinsTab); var capesTab = Button.builder(Component.translatable("skins.nav.capes"), btn -> { navBar.forEach(w -> { @@ -137,19 +132,19 @@ protected void init() { skinList.visible = skinList.active = false; capesList.visible = capesList.active = true; this.capesTab = true; - }).pos(width * 3 / 4 + 2, haF.getHeaderHeight()).width(100).build(); + }).pos(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { - haF.visitWidgets(this::removeWidget); + removeWidget(back); removeWidget(loadingPlaceholder); addRenderableWidget(current); addRenderableWidget(skinsTab); addRenderableWidget(capesTab); addRenderableWidget(skinList); addRenderableWidget(capesList); - haF.visitWidgets(this::addRenderableWidget); + addRenderableWidget(back); }; if (cachedProfile != null) { initDisplay(); @@ -465,7 +460,9 @@ public Entry(int height, SkinWidget widget, @Nullable Component label) { } } btn.active = true; - }, Component.translatable("skins.manage.delete.confirm"), Component.translatable("skins.manage.delete.confirm.desc") + }, Component.translatable("skins.manage.delete.confirm"), asset.active() ? + Component.translatable("skins.manage.delete.confirm.desc_active") : + Component.translatable("skins.manage.delete.confirm.desc") .withColor(Colors.RED.toInt()))); }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11) .build(); @@ -616,27 +613,27 @@ public void submit() { } @Override - public void buildVertices(VertexConsumer vertexConsumer, float f) { + public void buildVertices(VertexConsumer vertexConsumer, float z) { //top - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), z).setColor(this.col1()); //left - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y0() + gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), z).setColor(this.col1()); //bottom - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0() + gradientWidth(), this.y1() - gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), z).setColor(this.col1()); //right - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y0() + gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1() - gradientWidth(), this.y1() - gradientWidth(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), z).setColor(this.col1()); } @Nullable diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index b46e9d803..eb7677faf 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -43,7 +43,6 @@ import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.NotNull; public class SkinManager { @@ -67,7 +66,7 @@ public Skin read(Path p) { public CompletableFuture loadSkin(Skin skin) { - var rl = getRl(skin); + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); if (loadedTextures.contains(rl)) { return CompletableFuture.completedFuture(rl); } @@ -90,7 +89,7 @@ public CompletableFuture loadSkin(Skin skin) { } public AxoIdentifier loadCape(Cape cape) { - var rl = getRl(cape); + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); if (loadedTextures.contains(rl)) { return rl; } @@ -118,14 +117,6 @@ public void releaseAll() { loadedTextures.clear(); } - private @NotNull AxoIdentifier getRl(Skin skin) { - return AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); - } - - private @NotNull AxoIdentifier getRl(Cape cape) { - return AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); - } - public String getDefaultSkinHash(Account account) { var skin = DefaultPlayerSkin.get(UUIDHelper.fromUndashed(account.getUuid())); var mc = Minecraft.getInstance(); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java index a1f66bebb..cd5652e6b 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderState.java @@ -41,8 +41,7 @@ public record SkinRenderState(boolean classicVariant, float scale, @Nullable ScreenRectangle scissorArea, @Nullable ScreenRectangle bounds, - SkinRenderer renderer, - int color) implements PictureInPictureRenderState, IdentifiablePiPRenderState { + SkinRenderer renderer) implements PictureInPictureRenderState, IdentifiablePiPRenderState { public SkinRenderState(boolean classicVariant, ResourceLocation skinTexture, @@ -56,8 +55,7 @@ public SkinRenderState(boolean classicVariant, int y1, float scale, @Nullable ScreenRectangle scissorArea, - SkinRenderer renderer, - int color) { - this(classicVariant, skinTexture, cape, rotationX, rotationY, pivotY, x0, y0, x1, y1, scale, scissorArea, PictureInPictureRenderState.getBounds(x0, y0, x1, y1, scissorArea), renderer, color); + SkinRenderer renderer) { + this(classicVariant, skinTexture, cape, rotationX, rotationY, pivotY, x0, y0, x1, y1, scale, scissorArea, PictureInPictureRenderState.getBounds(x0, y0, x1, y1, scissorArea), renderer); } } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 595590dd6..523b8b050 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -87,13 +87,14 @@ protected void renderToTexture(SkinRenderState renderState, PoseStack poseStack) poseStack.translate(0.0F, -1.6010001F, 0.0F); var model = renderState.classicVariant() ? classicModel : slimModel; RenderType renderType = model.renderType(renderState.skinTexture()); - model.renderToBuffer(poseStack, this.bufferSource.getBuffer(renderType), 15728880, OverlayTexture.NO_OVERLAY, renderState.color()); + model.renderToBuffer(poseStack, this.bufferSource.getBuffer(renderType), 15728880, OverlayTexture.NO_OVERLAY); if (renderState.cape() != null) { if (capeModel == null) { capeModel = new PlayerCapeModel<>(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER_CAPE)); } var type = capeModel.renderType(renderState.cape()); - capeModel.renderToBuffer(poseStack, bufferSource.getBuffer(type), 15728880, OverlayTexture.NO_OVERLAY, renderState.color()); + poseStack.mulPose(Axis.XP.rotationDegrees(6.0F)); + capeModel.renderToBuffer(poseStack, bufferSource.getBuffer(type), 15728880, OverlayTexture.NO_OVERLAY); } this.bufferSource.endBatch(); matrix4fStack.popMatrix(); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index df4e68df2..f9a186cc8 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -107,7 +107,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo var renderer = SkinRenderer.getOrCreate(minecraft.renderBuffers().bufferSource(), minecraft, "" + hashCode()); ((GuiGraphicsAccessor) guiGraphics).getGuiRenderState() .submitPicturesInPictureState( - new SkinRenderState(classic, (ResourceLocation) skinRl, (ResourceLocation) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getRight(), this.getBottom(), scale, guiGraphics.scissorStack.peek(), renderer, -1)); + new SkinRenderState(classic, (ResourceLocation) skinRl, (ResourceLocation) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getRight(), this.getBottom(), scale, guiGraphics.scissorStack.peek(), renderer)); } @Override diff --git a/1.21.7/src/main/java/io/github/axolotlclient/util/HorizontalGradientRectangleRenderState.java b/1.21.7/src/main/java/io/github/axolotlclient/util/HorizontalGradientRectangleRenderState.java index f10f151d0..0d010d68e 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/util/HorizontalGradientRectangleRenderState.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/util/HorizontalGradientRectangleRenderState.java @@ -58,11 +58,11 @@ public void submit() { } @Override - public void buildVertices(VertexConsumer vertexConsumer, float f) { - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), f).setColor(this.col1()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), f).setColor(this.col2()); - vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), f).setColor(this.col2()); + public void buildVertices(VertexConsumer vertexConsumer, float z) { + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y0(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x0(), this.y1(), z).setColor(this.col1()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y1(), z).setColor(this.col2()); + vertexConsumer.addVertexWith2DPose(this.pose(), this.x1(), this.y0(), z).setColor(this.col2()); } @Nullable diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index ba7008639..1b260737d 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth; +import io.github.axolotlclient.modules.auth.skin.SkinManagementScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screen.ConfirmScreen; @@ -36,6 +37,7 @@ public class AccountsScreen extends Screen { private ButtonWidget loginButton; private ButtonWidget deleteButton; private ButtonWidget refreshButton; + private ButtonWidget skinsButton; public AccountsScreen(Screen currentScreen) { super(Text.translatable("accounts")); @@ -76,8 +78,13 @@ public void init() { accountsListWidget.setAccounts(Auth.getInstance().getAccounts()); - addDrawableSelectableElement(loginButton = new ButtonWidget.Builder(Text.translatable("auth.login"), - buttonWidget -> login()).positionAndSize(this.width / 2 - 154, this.height - 52, 150, 20).build()); + addDrawableSelectableElement(loginButton = ButtonWidget.builder(Text.translatable("auth.login"), + buttonWidget -> login()).positionAndSize(this.width / 2 - 154, this.height - 52, 100, 20).build()); + + addDrawableSelectableElement(skinsButton = ButtonWidget.builder(Text.translatable("skins.manage"), + btn -> client.setScreen(new SkinManagementScreen( + this, accountsListWidget.getSelectedOrNull().getAccount()))) + .positionAndSize(this.width / 2 - 50, this.height - 52, 100, 20).build()); this.addDrawableSelectableElement(ButtonWidget.builder(Text.translatable("auth.add"), button -> { @@ -94,7 +101,7 @@ public void init() { }, Text.translatable("auth.add.choose"), Text.empty(), Text.translatable("auth.add.offline"), Text.translatable("auth.add.ms"))); } }) - .positionAndSize(this.width / 2 + 4, this.height - 52, 150, 20).build()); + .positionAndSize(this.width / 2 + 4 + 50, this.height - 52, 100, 20).build()); this.deleteButton = this.addDrawableSelectableElement(ButtonWidget.builder(Text.translatable("selectServer.delete"), button -> { AccountsListWidget.Entry entry = this.accountsListWidget.getSelectedOrNull(); @@ -138,9 +145,10 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (client.world == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = true; + refreshButton.active = skinsButton.active = !entry.getAccount().isOffline(); + deleteButton.active = true; } else { - loginButton.active = deleteButton.active = refreshButton.active = false; + loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } } diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 726ea4fd3..13238701d 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -33,6 +33,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.mixin.MinecraftClientAccessor; import io.github.axolotlclient.modules.Module; +import io.github.axolotlclient.modules.auth.skin.SkinManager; import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.notifications.Notifications; import io.github.axolotlclient.util.options.GenericOption; @@ -54,10 +55,13 @@ public class Auth extends Accounts implements Module { @Getter private final static Auth Instance = new Auth(); public final BooleanOption showButton = new BooleanOption("auth.showButton", false); + public final BooleanOption skinManagerAnimations = new BooleanOption("skins.manage.animations", true); private final MinecraftClient client = MinecraftClient.getInstance(); private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> client.setScreen(new AccountsScreen(client.currentScreen))); private final Set loadingTexture = new HashSet<>(); private final Map textures = new WeakHashMap<>(); + @Getter + private final SkinManager skinManager = new SkinManager(); @Override public void init() { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java new file mode 100644 index 000000000..3059c7db2 --- /dev/null +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -0,0 +1,629 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.Watcher; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.ElementPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.navigation.GuiNavigationEvent; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.DraggingWidget; +import net.minecraft.client.gui.widget.LoadingTextWidget; +import net.minecraft.client.gui.widget.button.ButtonWidget; +import net.minecraft.client.gui.widget.button.SpriteButtonWidget; +import net.minecraft.client.gui.widget.list.ElementListWidget; +import net.minecraft.client.gui.widget.text.AbstractTextWidget; +import net.minecraft.client.gui.widget.text.TextWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.text.CommonTexts; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinManagementScreen extends Screen { + private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); + private static final int LIST_SKIN_WIDTH = 75; + private static final int LIST_SKIN_HEIGHT = 110; + private static final Text TEXT_EQUIPPING = Text.translatable("skins.manage.equipping"); + private final Screen parent; + private final Account account; + private MSApi.MCProfile cachedProfile; + private SkinListWidget skinList; + private SkinListWidget capesList; + private boolean capesTab; + private SkinWidget current; + private final Watcher skinDirWatcher; + + public SkinManagementScreen(Screen parent, Account account) { + super(Text.translatable("skins.manage")); + this.parent = parent; + this.account = account; + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + } + + @Override + protected void init() { + int headerHeight = 33; + int contentHeight = height - headerHeight * 2; + + addDrawableSelectableElement(new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight/2, width, textRenderer.fontHeight, getTitle(), textRenderer)); + var back = addDrawableSelectableElement(ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) + .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); + + var loadingPlaceholder = new LoadingTextWidget(textRenderer, Text.translatable("skins.loading")); + loadingPlaceholder.setDimensionsAndPosition(width, contentHeight, 0, headerHeight); + addDrawableSelectableElement(loadingPlaceholder); + addDrawableSelectableElement(back); + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); + skinList.setX(width / 2); + capesList.setX(width / 2); + var currentHeight = Math.min((width / 2f) * 120 / 85, contentHeight); + var currentWidth = currentHeight * 85 / 120; + current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); + current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); + + if (!capesTab) { + capesList.visible = capesList.active = false; + } else { + skinList.visible = skinList.active = false; + } + List navBar = new ArrayList<>(); + var skinsTab = ButtonWidget.builder(Text.translatable("skins.nav.skins"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = true; + capesList.visible = capesList.active = false; + capesTab = false; + }).position(width * 3 / 4 - 102, headerHeight).width(100).build(); + navBar.add(skinsTab); + var capesTab = ButtonWidget.builder(Text.translatable("skins.nav.capes"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = false; + capesList.visible = capesList.active = true; + this.capesTab = true; + }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); + navBar.add(capesTab); + skinsTab.active = this.capesTab; + capesTab.active = !this.capesTab; + Runnable addWidgets = () -> { + remove(back); + remove(loadingPlaceholder); + addDrawableSelectableElement(current); + addDrawableSelectableElement(skinsTab); + addDrawableSelectableElement(capesTab); + addDrawableSelectableElement(skinList); + addDrawableSelectableElement(capesList); + addDrawableSelectableElement(back); + }; + if (cachedProfile != null) { + initDisplay(); + addWidgets.run(); + return; + } + CompletableFuture fut; + if (account.needsRefresh()) { + fut = account.refresh(Auth.getInstance().getMsApi()); + } else { + fut = CompletableFuture.completedFuture(null); + } + fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAcceptAsync(profile -> { + cachedProfile = profile; + initDisplay(); + addWidgets.run(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + var error = Text.translatable("skins.error.failed_to_load"); + var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); + remove(loadingPlaceholder); + addDrawableSelectableElement(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); + addDrawableSelectableElement(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc, textRenderer)); + return null; + }); + } + + private void initDisplay() { + loadSkinsList(); + loadCapesList(); + } + + private void refreshCurrentList() { + if (capesTab) { + var scroll = capesList.getScrollAmount(); + loadCapesList(); + capesList.setScrollAmount(scroll); + } else { + var scroll = skinList.getScrollAmount(); + loadSkinsList(); + skinList.setScrollAmount(scroll); + } + } + + private void loadCapesList() { + capesList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + var capes = profile.capes(); + var deselectCape = createWidgetForCape(current.getSkin(), null); + var activeCape = capes.stream().filter(Cape::active).findFirst(); + current.setCape(activeCape.orElse(null)); + deselectCape.noCape(activeCape.isEmpty()); + for (int i = 0; i < capes.size() + 1; i += columns) { + Entry widget; + if (i == 0) { + widget = createEntry(capesList.getEntryContentsHeight(), deselectCape, Text.translatable("skins.capes.no_cape")); + } else { + var cape = capes.get(i - 1); + widget = createEntryForCape(current.getSkin(), cape, capesList.getEntryContentsHeight()); + } + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < capes.size() + 1 - c)) continue; + var cape2 = capes.get(i + c - 1); + Entry widget2 = createEntryForCape(current.getSkin(), cape2, capesList.getEntryContentsHeight()); + + widgets.add(widget2); + } + capesList.addEntry(new Row(widgets)); + } + } + + private void loadSkinsList() { + skinList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + List skins = new ArrayList<>(profile.skins()); + var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); + var local = new ArrayList<>(loadLocalSkins()); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + skins.replaceAll(s -> { + if (s instanceof MSApi.MCProfile.OnlineSkin online) { + if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { + local.remove(localHashes.remove(s.textureKey())); + return new Skin.Shared(file, online); + } + } + return s; + }); + //local.removeIf(s -> hashes.contains(s.textureKey())); + skins.addAll(local); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } + populateSkinList(skins, columns); + } + + private List loadLocalSkins() { + try { + Files.createDirectories(SKINS_DIR); + try (Stream skins = Files.list(SKINS_DIR)) { + return skins.filter(Files::isRegularFile).sorted(Comparator.comparingLong(p -> { + try { + return Files.getLastModifiedTime(p).toMillis(); + } catch (IOException e) { + return 0L; + } + }).reversed()).map(Auth.getInstance().getSkinManager()::read).filter(Objects::nonNull).toList(); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to read skins dir!", e); + } + return Collections.emptyList(); + } + + private void populateSkinList(List skins, int columns) { + int entryHeight = skinList.getEntryContentsHeight(); + for (int i = 0; i < skins.size(); i += columns) { + var s = skins.get(i); + if (s != null && s.active()) { + current.setSkin(s); + } + var widget = createEntryForSkin(s, entryHeight); + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < skins.size() - c)) continue; + var s2 = skins.get(i + c); + if (s2 != null && s2.active()) { + current.setSkin(s2); + } + var widget2 = createEntryForSkin(s2, entryHeight); + widgets.add(widget2); + } + skinList.addEntry(new Row(widgets)); + } + } + + @Override + public void filesDragged(List packs) { + packs.forEach(p -> { + try { + Files.copy(p, SKINS_DIR.resolve(p.getFileName())); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }); + loadSkinsList(); + } + + private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); + } + + private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { + return createEntry(entryHeight, createWidgetForCape(currentSkin, cape), Text.literal(cape.alias())); + } + + private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account); + widget2.setRotationY(210); + return widget2; + } + + @Override + protected void clearChildren() { + super.clearChildren(); + SkinRenderer.closeRenderers(); + Auth.getInstance().getSkinManager().releaseAll(); + } + + @Override + public void removed() { + Auth.getInstance().getSkinManager().releaseAll(); + Watcher.close(skinDirWatcher); + SkinRenderer.closeRenderers(); + } + + @Override + public void closeScreen() { + client.setScreen(parent); + } + + private SkinListWidget getCurrentList() { + return capesTab ? capesList : skinList; + } + + private class SkinListWidget extends ElementListWidget { + public SkinListWidget(MinecraftClient minecraft, int width, int height, int y, int entryHeight) { + super(minecraft, width, height, y, entryHeight); + } + + @Override + public int addEntry(Row entry) { + return super.addEntry(entry); + } + + @Override + protected int getScrollbarPositionX() { + return getXEnd() - 8; + } + + @Override + public int getRowLeft() { + return getX() + 3; + } + + @Override + public int getRowWidth() { + if (!canScroll()) { + return getWidth() - 4; + } + return getWidth() - 14; + } + + public int getEntryContentsHeight() { + return itemHeight - 4; + } + + @Override + public @Nullable ElementPath nextFocusPath(GuiNavigationEvent event) { + if (!active || !visible) return null; + return super.nextFocusPath(event); + } + + @Override + public void clearEntries() { + super.clearEntries(); + } + + @Override + public void centerScrollOn(Row entry) { + super.centerScrollOn(entry); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amountX, double amountY) { + if (!visible) return false; + return super.mouseScrolled(mouseX, mouseY, amountX, amountY); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return active && visible && super.isMouseOver(mouseX, mouseY); + } + } + + private class Row extends ElementListWidget.Entry { + private final List widgets; + + public Row(List entries) { + this.widgets = entries; + } + + @Override + public @NotNull List selectableChildren() { + return widgets; + } + + @Override + public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + int x = left; + if (widgets.isEmpty()) return; + int count = widgets.size(); + int padding = ((width - 5 * (count - 1)) / count); + for (var w : widgets) { + w.setPosition(x, top); + w.setWidth(padding); + w.render(guiGraphics, mouseX, mouseY, partialTick); + x += w.getWidth() + 5; + } + } + + @Override + public @NotNull List children() { + return widgets; + } + + @Override + public void setFocusedChild(@Nullable Element focused) { + super.setFocusedChild(focused); + if (focused != null) { + getCurrentList().centerScrollOn(this); + } + } + } + + Entry createEntry(int height, SkinWidget widget) { + return createEntry(height, widget, null); + } + + Entry createEntry(int height, SkinWidget widget, Text label) { + return new Entry(height, widget, label); + } + + private class Entry extends DraggingWidget { + private final SkinWidget skinWidget; + private final @Nullable ClickableWidget label; + private final List actionButtons = new ArrayList<>(); + private final ClickableWidget equipButton; + private boolean equipping; + private long equippingStart; + + public Entry(int height, SkinWidget widget, @Nullable Text label) { + super(0, 0, widget.getWidth(), height, Text.empty()); + widget.setWidth(getWidth() - 4); + var asset = widget.getFocusedAsset(); + if (asset != null) { + if (asset.isLocal()) { + var delete = SpriteButtonWidget.builder(Text.translatable("skins.manage.delete"), btn -> { + btn.active = false; + client.setScreen(new ConfirmScreen(confirmed -> { + client.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); + } + } + btn.active = true; + }, Text.translatable("skins.manage.delete.confirm"), asset.active() ? + Text.translatable("skins.manage.delete.confirm.desc_active") : + Text.translatable("skins.manage.delete.confirm.desc") + .setColor(Colors.RED.toInt()))); + }, true).sprite(Identifier.of("axolotlclient", "delete"), 7, 7).dimensions(11, 11) + .build(); + delete.setTooltip(Tooltip.create(delete.getMessage())); + this.actionButtons.add(delete); + } + if (asset.supportsDownload() && !asset.isLocal()) { + var download = SpriteButtonWidget.builder(Text.translatable("skins.manage.download"), btn -> { + btn.active = false; + asset.image().thenAcceptAsync(b -> { + try { + var out = SKINS_DIR.resolve(asset.textureKey()); + Files.createDirectories(out.getParent()); + Files.write(out, b); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); + } + refreshCurrentList(); + btn.active = true; + }); + }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); + download.setTooltip(Tooltip.create(download.getMessage())); + this.actionButtons.add(download); + } + } + if (label != null) { + this.label = new AbstractTextWidget(0, 0, widget.getWidth(), 16, label, textRenderer) { + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + drawScrollingText(guiGraphics, textRenderer, 2, -1); + } + }; + this.label.active = false; + } else { + this.label = null; + } + this.equipButton = ButtonWidget.builder(Text.translatable( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> { + equippingStart = Util.getMeasuringTimeMs(); + equipping = true; + btn.setMessage(TEXT_EQUIPPING); + btn.active = false; + widget.equip().thenAcceptAsync(p -> { + cachedProfile = p; + refreshCurrentList(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + return null; + }); + }).width(widget.getWidth()).build(); + this.equipButton.active = !widget.isEquipped(); + this.skinWidget = widget; + } + + @Override + public @NotNull List children() { + return Stream.concat(actionButtons.stream(), Stream.of(skinWidget, label, equipButton)).filter(Objects::nonNull).toList(); + } + + private float applyEasing(float x) { + return x * x * x; + } + + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int y = getY() + 4; + int x = getX() + 2; + if (skinWidget.isEquipped() || equipping) { + long prog; + if (Auth.getInstance().skinManagerAnimations.get()) { + if (equipping) prog = (Util.getMeasuringTimeMs() - equippingStart) / 20 % 100; + else prog = Math.abs((Util.getMeasuringTimeMs() / 30 % 200) - 100); + } else prog = 100; + var percent = (prog / 100f); + float gradientWidth; + if (equipping) { + gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); + } else { + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + applyEasing(percent) * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + } + GradientHoleRectangleRenderState.render(guiGraphics, getX() + 2, getY() + 2, getXEnd() - 2, + skinWidget.getYEnd() + 2, + gradientWidth, + equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); + } + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); + skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); + int actionButtonY = getY() + 2; + for (var button : actionButtons) { + button.setPosition(skinWidget.getXEnd() - button.getWidth(), actionButtonY); + if (isHovered() || button.isHoveredOrFocused()) { + button.render(guiGraphics, mouseX, mouseY, partialTick); + } + actionButtonY += button.getHeight() + 2; + } + if (label != null) { + label.setPosition(x, skinWidget.getYEnd() + 6); + label.render(guiGraphics, mouseX, mouseY, partialTick); + label.setWidth(getWidth() - 4); + equipButton.setPosition(x, label.getYEnd() + 2); + } else { + equipButton.setPosition(x, skinWidget.getYEnd() + 4); + } + equipButton.setWidth(getWidth() - 4); + equipButton.render(guiGraphics, mouseX, mouseY, partialTick); + + if (isHovered()) { + guiGraphics.br$outlineRect(getX(), getY(), getWidth(), getHeight(), -1); + } + } + + @Override + protected void updateNarration(NarrationMessageBuilder narrationElementOutput) { + skinWidget.appendNarrations(narrationElementOutput); + actionButtons.forEach(w -> w.appendNarrations(narrationElementOutput)); + if (label != null) { + label.appendNarrations(narrationElementOutput); + } + equipButton.appendNarrations(narrationElementOutput); + } + + private class GradientHoleRectangleRenderState { + + public static void render(GuiGraphics graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { + var vertexConsumer = graphics.getVertexConsumers().getBuffer(RenderLayer.getGui()); + float z = 0; + //top + var pose = graphics.getMatrices().peek().getModel(); + vertexConsumer.xyz(pose, x0, y0, z).color(col1); + vertexConsumer.xyz(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x1, y0, z).color(col1); + //left + vertexConsumer.xyz(pose, x0, y1, z).color(col1); + vertexConsumer.xyz(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x0, y0, z).color(col1); + //bottom + vertexConsumer.xyz(pose, x1, y1, z).color(col1); + vertexConsumer.xyz(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x0, y1, z).color(col1); + //right + vertexConsumer.xyz(pose, x1, y0, z).color(col1); + vertexConsumer.xyz(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.xyz(pose, x1, y1, z).color(col1); + } + } + } +} diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java new file mode 100644 index 000000000..f115d88ea --- /dev/null +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; + +import com.google.common.hash.Hashing; +import com.mojang.blaze3d.texture.NativeImage; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.AxoMinecraftClient; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.util.ClientColors; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.util.Identifier; + +public class SkinManager { + + private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + + public Skin read(Path p) { + boolean slim; + String sha256; + try { + var in = Files.readAllBytes(p); + sha256 = Hashing.sha256().hashBytes(in).toString(); + try (var img = NativeImage.read(in)) { + slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + } + return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + } catch (Exception e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + } + return null; + } + + + public CompletableFuture loadSkin(Skin skin) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); + if (loadedTextures.contains(rl)) { + return CompletableFuture.completedFuture(rl); + } + + return skin.image().thenApplyAsync(bytes -> { + try { + var tex = new NativeImageBackedTexture(NativeImage.read(bytes)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((v, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load skin!", t); + } + return v; + }); + } + + public AxoIdentifier loadCape(Cape cape) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); + if (loadedTextures.contains(rl)) { + return rl; + } + + return cape.image().thenApplyAsync(bytes -> { + try { + var tex = new NativeImageBackedTexture(NativeImage.read(bytes)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((id, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load cape!", t); + } + return id; + }).getNow(null); + + } + + public void releaseAll() { + loadedTextures.forEach(id -> MinecraftClient.getInstance().getTextureManager().destroyTexture((Identifier) id)); + loadedTextures.clear(); + } + + public String getDefaultSkinHash(Account account) { + var skin = DefaultSkinHelper.getSkin(UUIDHelper.fromUndashed(account.getUuid())); + var mc = MinecraftClient.getInstance(); + var resourceManager = mc.getResourceManager(); + try { + var res = resourceManager.getResourceOrThrow(skin.texture()); + try ( + var in = res.open()) { + return Hashing.sha256().hashBytes(in.readAllBytes()).toString(); + } + } catch (IOException ignored) { + } + return null; + } +} diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java new file mode 100644 index 000000000..0c0998395 --- /dev/null +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.mojang.blaze3d.lighting.DiffuseLighting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.model.EntityModelLayers; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Axis; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinRenderer { + private static final Map renderers = new ConcurrentHashMap<>(); + + public static void closeRenderers() { + renderers.clear(); + } + + public static SkinRenderer getOrCreate(MinecraftClient minecraft, String id) { + return renderers.computeIfAbsent(id, _id -> new SkinRenderer(minecraft, id)); + } + + private PlayerEntityModel classicModel, slimModel; + private final MinecraftClient minecraft; + private final String id; + + private SkinRenderer(MinecraftClient minecraft, String id) { + this.minecraft = minecraft; + this.id = id; + } + + public void render(GuiGraphics graphics, boolean classicVariant, + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { + if (classicModel == null) { + classicModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER), false); + classicModel.child = false; + } + if (slimModel == null) { + slimModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER_SLIM), true); + slimModel.child = false; + } + + int width = x1- x0; + graphics.getMatrices().push(); + graphics.getMatrices().translate(x0 + width / 2.0F, (float)(y1), 100.0F); + graphics.getMatrices().scale(scale, scale, scale); + graphics.getMatrices().translate(0.0F, -0.0625F, 0.0F); + graphics.getMatrices().rotateAround(Axis.X_POSITIVE.rotationDegrees(rotationX), 0.0F, pivotY, 0.0F); + graphics.getMatrices().rotate(Axis.Y_POSITIVE.rotationDegrees(rotationY)); + graphics.draw(); + DiffuseLighting.setupInventoryShaderLighting(Axis.X_POSITIVE.rotationDegrees(rotationX)); + graphics.getMatrices().push(); + graphics.getMatrices().scale(1.0F, 1.0F, -1.0F); + graphics.getMatrices().translate(0.0F, -1.5F, 0.0F); + var model = classicVariant ? classicModel : slimModel; + RenderLayer renderLayer = model.getLayer(skinTexture); + model.method_60879(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(renderLayer), 15728880, OverlayTexture.DEFAULT_UV); + if (cape != null) { + graphics.getMatrices().translate(0.0F, 0.0F, 0.125F); + graphics.getMatrices().rotate(Axis.X_POSITIVE.rotationDegrees(6.0F)); + graphics.getMatrices().rotate(Axis.Y_POSITIVE.rotationDegrees(180.0F)); + model.renderCape(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); + } + graphics.getMatrices().pop(); + graphics.draw(); + DiffuseLighting.setup3DGuiLighting(); + graphics.getMatrices().pop(); + } + + protected @NotNull String getTextureLabel() { + return "axolotlclient/skin_render/" + id; + } +} diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java new file mode 100644 index 000000000..f776bf9d0 --- /dev/null +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.ElementPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.GuiNavigationEvent; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.client.texture.PlayerSkin; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.text.CommonTexts; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class SkinWidget extends ClickableWidget { + private static final float MODEL_HEIGHT = 2.125F; + private static final float FIT_SCALE = 0.97F; + private static final float ROTATION_SENSITIVITY = 2.5F; + private static final float DEFAULT_ROTATION_X = -5.0F; + private static final float DEFAULT_ROTATION_Y = 30.0F; + private static final float ROTATION_X_LIMIT = 50.0F; + private float rotationX = DEFAULT_ROTATION_X; + @Setter + private float rotationY = DEFAULT_ROTATION_Y; + @Getter + @Setter + private Skin skin; + @Getter + @Setter + private Cape cape; + private final Account owner; + private boolean noCape, noCapeActive; + + public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { + super(0, 0, width, height, CommonTexts.EMPTY); + this.skin = skin; + this.cape = cape; + this.owner = owner; + } + + public SkinWidget(int width, int height, Skin skin, Account owner) { + this(width, height, skin, null, owner); + } + + public void noCape(boolean noCapeActive) { + noCape = true; + this.noCapeActive = noCapeActive; + } + + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + var minecraft = MinecraftClient.getInstance(); + + float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; + float pivotY = -1.0625F; + + AxoIdentifier skinRl; + boolean classic; + SkinManager skinManager = Auth.getInstance().getSkinManager(); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); + if (loader != null && loader.isDone()) { + skinRl = loader.join(); + classic = skin.isClassicVariant(); + } else { + var skin = DefaultSkinHelper.getSkin(UUIDHelper.fromUndashed(owner.getUuid())); + classic = skin.model() == PlayerSkin.Model.WIDE; + skinRl = skin.texture(); + } + var capeRl = cape == null ? null : skinManager.loadCape(cape); + + // You might say that using `hashCode()` like this isn't ideal, but in reality it doesn't matter. These objects get freed + // correctly by the screen so we mostly only need unique identifiers per widget which `hashCode()` provides. + var renderer = SkinRenderer.getOrCreate(minecraft, "" + hashCode()); + + renderer.render(guiGraphics, classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getXEnd(), this.getYEnd(), scale); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) { + this.rotationX = MathHelper.clamp(this.rotationX - (float) dragY * ROTATION_SENSITIVITY, -ROTATION_X_LIMIT, ROTATION_X_LIMIT); + this.rotationY += (float) dragX * ROTATION_SENSITIVITY; + } + + @Override + public void playDownSound(SoundManager handler) { + } + + @Override + protected void updateNarration(NarrationMessageBuilder builder) { + + } + + @Override + public @Nullable ElementPath nextFocusPath(GuiNavigationEvent event) { + return null; + } + + public boolean isEquipped() { + return noCape ? noCapeActive : (cape != null ? cape.active() : skin != null && skin.active()); + } + + public CompletableFuture equip() { + var msApi = Auth.getInstance().getMsApi(); + if (noCape) { + return msApi.hideCape(owner); + } + if (cape != null) { + return cape.equip(msApi, owner); + } + if (skin != null) { + return skin.equip(msApi, owner); + } + return msApi.resetSkin(owner); + } + + public Asset getFocusedAsset() { + return noCape ? null : cape != null ? cape : skin; + } +} diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index 653cb0168..a4b65551c 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -824,7 +824,8 @@ "skins.manage.equipping": "Equipping...", "skins.manage.delete": "Delete Skin", "skins.manage.delete.confirm": "Confirm Deletion", - "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", + "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure?", + "skins.manage.delete.confirm.desc_active": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", "skins.manage.animations": "Skin Manger Animations", "skins.manage.download": "Download Skin" } From 77cdec7506494c23fd5b9ff4d696c22717b26d05 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 31 Aug 2025 19:37:02 +0200 Subject: [PATCH 07/23] port to 1.20.1 --- .../modules/auth/AccountsScreen.java | 16 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../auth/skin/SkinManagementScreen.java | 739 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 134 ++++ .../modules/auth/skin/SkinRenderer.java | 90 +++ .../modules/auth/skin/SkinWidget.java | 147 ++++ .../modules/auth/skin/SkinRenderer.java | 4 +- .../auth/skin/SkinManagementScreen.java | 4 +- .../modules/auth/skin/SkinRenderer.java | 57 +- .../modules/auth/skin/SkinWidget.java | 6 +- 10 files changed, 1149 insertions(+), 54 deletions(-) create mode 100644 1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java create mode 100644 1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java create mode 100644 1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java create mode 100644 1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 72df37020..15f0dff53 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth; +import io.github.axolotlclient.modules.auth.skin.SkinManagementScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screen.ConfirmScreen; @@ -36,6 +37,7 @@ public class AccountsScreen extends Screen { private ButtonWidget loginButton; private ButtonWidget deleteButton; private ButtonWidget refreshButton; + private ButtonWidget skinsButton; public AccountsScreen(Screen currentScreen) { super(Text.translatable("accounts")); @@ -82,7 +84,12 @@ public void init() { accountsListWidget.setAccounts(Auth.getInstance().getAccounts()); addDrawableChild(loginButton = new ButtonWidget.Builder(Text.translatable("auth.login"), - buttonWidget -> login()).positionAndSize(this.width / 2 - 154, this.height - 52, 150, 20).build()); + buttonWidget -> login()).positionAndSize(this.width / 2 - 154, this.height - 52, 100, 20).build()); + + addDrawableChild(skinsButton = ButtonWidget.builder(Text.translatable("skins.manage"), + btn -> client.setScreen(new SkinManagementScreen( + this, accountsListWidget.getSelectedOrNull().getAccount()))) + .positionAndSize(this.width / 2 - 50, this.height - 52, 100, 20).build()); this.addDrawableChild(ButtonWidget.builder(Text.translatable("auth.add"), button -> { @@ -99,7 +106,7 @@ public void init() { }, Text.translatable("auth.add.choose"), Text.empty(), Text.translatable("auth.add.offline"), Text.translatable("auth.add.ms"))); } }) - .positionAndSize(this.width / 2 + 4, this.height - 52, 150, 20).build()); + .positionAndSize(this.width / 2 + 4 + 50, this.height - 52, 100, 20).build()); this.deleteButton = this.addDrawableChild(ButtonWidget.builder(Text.translatable("selectServer.delete"), button -> { AccountsListWidget.Entry entry = this.accountsListWidget.getSelectedOrNull(); @@ -143,9 +150,10 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (client.world == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = true; + refreshButton.active = skinsButton.active = !entry.getAccount().isOffline(); + deleteButton.active = true; } else { - loginButton.active = deleteButton.active = refreshButton.active = false; + loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 00c023829..c59c5845e 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -34,6 +34,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.mixin.MinecraftClientAccessor; import io.github.axolotlclient.modules.Module; +import io.github.axolotlclient.modules.auth.skin.SkinManager; import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.notifications.Notifications; import io.github.axolotlclient.util.options.GenericOption; @@ -55,10 +56,13 @@ public class Auth extends Accounts implements Module { @Getter private final static Auth Instance = new Auth(); public final BooleanOption showButton = new BooleanOption("auth.showButton", false); + public final BooleanOption skinManagerAnimations = new BooleanOption("skins.manage.animations", true); private final MinecraftClient client = MinecraftClient.getInstance(); private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> client.setScreen(new AccountsScreen(client.currentScreen))); private final Set loadingTexture = new HashSet<>(); private final Map textures = new HashMap<>(); + @Getter + private final SkinManager skinManager = new SkinManager(); @Override public void init() { @@ -76,7 +80,7 @@ public void init() { current = new Account(client.getSession().getUsername(), UUIDHelper.toUndashed(client.getSession().getPlayerUuid()), client.getSession().getAccessToken()); } - category.add(showButton, viewAccounts); + category.add(showButton, viewAccounts, skinManagerAnimations); AxolotlClient.config().general.add(category); } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java new file mode 100644 index 000000000..b2de4f0c0 --- /dev/null +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -0,0 +1,739 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.Watcher; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.*; +import net.minecraft.client.gui.navigation.GuiNavigationEvent; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.LoadingDisplay; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.*; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.text.CommonTexts; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinManagementScreen extends Screen { + private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); + private static final int LIST_SKIN_WIDTH = 75; + private static final int LIST_SKIN_HEIGHT = 110; + private static final Text TEXT_EQUIPPING = Text.translatable("skins.manage.equipping"); + private final Screen parent; + private final Account account; + private MSApi.MCProfile cachedProfile; + private SkinListWidget skinList; + private SkinListWidget capesList; + private boolean capesTab; + private SkinWidget current; + private final Watcher skinDirWatcher; + + public SkinManagementScreen(Screen parent, Account account) { + super(Text.translatable("skins.manage")); + this.parent = parent; + this.account = account; + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + } + + @Override + protected void init() { + int headerHeight = 33; + int contentHeight = height - headerHeight * 2; + + addDrawableChild(new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle(), textRenderer)); + var back = addDrawableChild(ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) + .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); + + var loadingPlaceholder = new ClickableWidget(0, headerHeight, width, contentHeight, Text.translatable("skins.loading")) { + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + int centerX = this.getX() + this.getWidth() / 2; + int centerY = this.getY() + this.getHeight() / 2; + Text text = this.getMessage(); + graphics.drawText(textRenderer, text, centerX - textRenderer.getWidth(text) / 2, centerY - 9, -1, false); + String string = LoadingDisplay.get(Util.getMeasuringTimeMs()); + graphics.drawText(textRenderer, string, centerX - textRenderer.getWidth(string) / 2, centerY + 9, 0xFF808080, false); + } + + @Override + protected void updateNarration(NarrationMessageBuilder builder) { + + } + }; + loadingPlaceholder.active = false; + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); + skinList.setLeftPos(width / 2); + capesList.setLeftPos(width / 2); + var currentHeight = Math.min((width / 2f) * 120 / 85, contentHeight); + var currentWidth = currentHeight * 85 / 120; + current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); + current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); + + if (!capesTab) { + capesList.visible = capesList.active = false; + } else { + skinList.visible = skinList.active = false; + } + List navBar = new ArrayList<>(); + var skinsTab = ButtonWidget.builder(Text.translatable("skins.nav.skins"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = true; + capesList.visible = capesList.active = false; + capesTab = false; + }).position(width * 3 / 4 - 102, headerHeight).width(100).build(); + navBar.add(skinsTab); + var capesTab = ButtonWidget.builder(Text.translatable("skins.nav.capes"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = false; + capesList.visible = capesList.active = true; + this.capesTab = true; + }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); + navBar.add(capesTab); + skinsTab.active = this.capesTab; + capesTab.active = !this.capesTab; + Runnable addWidgets = () -> { + remove(back); + remove(loadingPlaceholder); + addDrawableChild(current); + addDrawableChild(skinList); + addDrawableChild(capesList); + addDrawableChild(skinsTab); + addDrawableChild(capesTab); + addDrawableChild(back); + }; + if (cachedProfile != null) { + initDisplay(); + addWidgets.run(); + return; + } + CompletableFuture fut; + if (account.needsRefresh()) { + fut = account.refresh(Auth.getInstance().getMsApi()); + } else { + fut = CompletableFuture.completedFuture(null); + } + fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAcceptAsync(profile -> { + cachedProfile = profile; + initDisplay(); + addWidgets.run(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + var error = Text.translatable("skins.error.failed_to_load"); + var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); + remove(loadingPlaceholder); + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc, textRenderer)); + return null; + }); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics); + super.render(graphics, mouseX, mouseY, delta); + } + + private void initDisplay() { + loadSkinsList(); + loadCapesList(); + } + + private void refreshCurrentList() { + if (capesTab) { + var scroll = capesList.getScrollAmount(); + loadCapesList(); + capesList.setScrollAmount(scroll); + } else { + var scroll = skinList.getScrollAmount(); + loadSkinsList(); + skinList.setScrollAmount(scroll); + } + } + + private void loadCapesList() { + capesList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + var capes = profile.capes(); + var deselectCape = createWidgetForCape(current.getSkin(), null); + var activeCape = capes.stream().filter(Cape::active).findFirst(); + current.setCape(activeCape.orElse(null)); + deselectCape.noCape(activeCape.isEmpty()); + for (int i = 0; i < capes.size() + 1; i += columns) { + Entry widget; + if (i == 0) { + widget = createEntry(capesList.getEntryContentsHeight(), deselectCape, Text.translatable("skins.capes.no_cape")); + } else { + var cape = capes.get(i - 1); + widget = createEntryForCape(current.getSkin(), cape, capesList.getEntryContentsHeight()); + } + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < capes.size() + 1 - c)) continue; + var cape2 = capes.get(i + c - 1); + Entry widget2 = createEntryForCape(current.getSkin(), cape2, capesList.getEntryContentsHeight()); + + widgets.add(widget2); + } + capesList.addEntry(new Row(widgets)); + } + } + + private void loadSkinsList() { + skinList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + List skins = new ArrayList<>(profile.skins()); + var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); + var local = new ArrayList<>(loadLocalSkins()); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + skins.replaceAll(s -> { + if (s instanceof MSApi.MCProfile.OnlineSkin online) { + if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { + local.remove(localHashes.remove(s.textureKey())); + return new Skin.Shared(file, online); + } + } + return s; + }); + //local.removeIf(s -> hashes.contains(s.textureKey())); + skins.addAll(local); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } + populateSkinList(skins, columns); + } + + private List loadLocalSkins() { + try { + Files.createDirectories(SKINS_DIR); + try (Stream skins = Files.list(SKINS_DIR)) { + return skins.filter(Files::isRegularFile).sorted(Comparator.comparingLong(p -> { + try { + return Files.getLastModifiedTime(p).toMillis(); + } catch (IOException e) { + return 0L; + } + }).reversed()).map(Auth.getInstance().getSkinManager()::read).filter(Objects::nonNull).toList(); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to read skins dir!", e); + } + return Collections.emptyList(); + } + + private void populateSkinList(List skins, int columns) { + int entryHeight = skinList.getEntryContentsHeight(); + for (int i = 0; i < skins.size(); i += columns) { + var s = skins.get(i); + if (s != null && s.active()) { + current.setSkin(s); + } + var widget = createEntryForSkin(s, entryHeight); + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < skins.size() - c)) continue; + var s2 = skins.get(i + c); + if (s2 != null && s2.active()) { + current.setSkin(s2); + } + var widget2 = createEntryForSkin(s2, entryHeight); + widgets.add(widget2); + } + skinList.addEntry(new Row(widgets)); + } + } + + @Override + public void filesDragged(List packs) { + packs.forEach(p -> { + try { + Files.copy(p, SKINS_DIR.resolve(p.getFileName())); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }); + loadSkinsList(); + } + + private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); + } + + private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { + return createEntry(entryHeight, createWidgetForCape(currentSkin, cape), Text.literal(cape.alias())); + } + + private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account); + widget2.setRotationY(210); + return widget2; + } + + @Override + protected void clearChildren() { + super.clearChildren(); + Auth.getInstance().getSkinManager().releaseAll(); + } + + @Override + public void removed() { + Auth.getInstance().getSkinManager().releaseAll(); + Watcher.close(skinDirWatcher); + } + + @Override + public void closeScreen() { + client.setScreen(parent); + } + + private SkinListWidget getCurrentList() { + return capesTab ? capesList : skinList; + } + + private class SkinListWidget extends ElementListWidget { + public boolean active = true, visible = true; + + public SkinListWidget(MinecraftClient minecraft, int width, int height, int y, int entryHeight) { + super(minecraft, width, SkinManagementScreen.this.height, y, y + height, entryHeight); + setRenderHeader(false, 0); + setRenderBackground(false); + } + + @Override + public int addEntry(Row entry) { + return super.addEntry(entry); + } + + @Override + protected int getScrollbarPositionX() { + return right - 8; + } + + @Override + public int getRowLeft() { + return left + 3; + } + + @Override + public int getRowWidth() { + if (!(getMaxScroll() > 0)) { + return width - 4; + } + return width - 14; + } + + public int getEntryContentsHeight() { + return itemHeight - 4; + } + + @Override + public @Nullable ElementPath nextFocusPath(GuiNavigationEvent event) { + if (!active || !visible) return null; + return super.nextFocusPath(event); + } + + @Override + public void clearEntries() { + super.clearEntries(); + } + + @Override + public void centerScrollOn(Row entry) { + super.centerScrollOn(entry); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amountY) { + if (!visible) return false; + return super.mouseScrolled(mouseX, mouseY, amountY); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return active && visible && super.isMouseOver(mouseX, mouseY); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (!visible) return; + super.render(graphics, mouseX, mouseY, delta); + } + } + + private class Row extends ElementListWidget.Entry { + private final List widgets; + + public Row(List entries) { + this.widgets = entries; + } + + @Override + public @NotNull List selectableChildren() { + return widgets; + } + + @Override + public void render(GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + int x = left; + if (widgets.isEmpty()) return; + int count = widgets.size(); + int padding = ((width - 5 * (count - 1)) / count); + for (var w : widgets) { + w.setPosition(x, top); + w.setWidth(padding); + w.render(guiGraphics, mouseX, mouseY, partialTick); + x += w.getWidth() + 5; + } + } + + @Override + public @NotNull List children() { + return widgets; + } + + @Override + public void setFocusedChild(@Nullable Element focused) { + super.setFocusedChild(focused); + if (focused != null) { + getCurrentList().centerScrollOn(this); + } + } + } + + Entry createEntry(int height, SkinWidget widget) { + return createEntry(height, widget, null); + } + + Entry createEntry(int height, SkinWidget widget, Text label) { + return new Entry(height, widget, label); + } + + private class Entry extends ClickableWidget implements ParentElement { + private final SkinWidget skinWidget; + private final @Nullable ClickableWidget label; + private final List actionButtons = new ArrayList<>(); + private final ClickableWidget equipButton; + private boolean equipping; + private long equippingStart; + @Nullable + private Element focused; + private boolean dragging; + + public Entry(int height, SkinWidget widget, @Nullable Text label) { + super(0, 0, widget.getWidth(), height, Text.empty()); + widget.setWidth(getWidth() - 4); + var asset = widget.getFocusedAsset(); + if (asset != null) { + if (asset.isLocal()) { + var delete = new ButtonWidget(0, 0, 11, 11, Text.translatable("skins.manage.delete"), btn -> { + btn.active = false; + client.setScreen(new ConfirmScreen(confirmed -> { + client.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); + } + } + btn.active = true; + }, Text.translatable("skins.manage.delete.confirm"), asset.active() ? + Text.translatable("skins.manage.delete.confirm.desc_active") : + (Text) Text.translatable("skins.manage.delete.confirm.desc") + .br$color(Colors.RED.toInt()))); + }, Supplier::get) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawTexture(SPRITE, getX(), getY(), 0, 0, 7, 7, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } + }; + delete.setTooltip(Tooltip.create(delete.getMessage())); + this.actionButtons.add(delete); + } + if (asset.supportsDownload() && !asset.isLocal()) { + var download = new ButtonWidget(0, 0, 11, 11, Text.translatable("skins.manage.download"), btn -> { + btn.active = false; + asset.image().thenAcceptAsync(b -> { + try { + var out = SKINS_DIR.resolve(asset.textureKey()); + Files.createDirectories(out.getParent()); + Files.write(out, b); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); + } + refreshCurrentList(); + btn.active = true; + }); + }, Supplier::get) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawTexture(SPRITE, getX(), getY(), 0, 0, 7, 7, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } + }; + download.setTooltip(Tooltip.create(download.getMessage())); + this.actionButtons.add(download); + } + } + if (label != null) { + this.label = new AbstractTextWidget(0, 0, widget.getWidth(), 16, label, textRenderer) { + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + drawScrollingText(guiGraphics, textRenderer, 2, -1); + } + }; + this.label.active = false; + } else { + this.label = null; + } + this.equipButton = ButtonWidget.builder(Text.translatable( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> { + equippingStart = Util.getMeasuringTimeMs(); + equipping = true; + btn.setMessage(TEXT_EQUIPPING); + btn.active = false; + widget.equip().thenAcceptAsync(p -> { + cachedProfile = p; + refreshCurrentList(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + return null; + }); + }).width(widget.getWidth()).build(); + this.equipButton.active = !widget.isEquipped(); + this.skinWidget = widget; + } + + @Override + public final boolean isDragging() { + return this.dragging; + } + + @Override + public final void setDragging(boolean dragging) { + this.dragging = dragging; + } + + @Nullable + @Override + public Element getFocused() { + return this.focused; + } + + @Override + public void setFocusedChild(@Nullable Element child) { + if (this.focused != null) { + this.focused.setFocused(false); + } + + if (child != null) { + child.setFocused(true); + } + + this.focused = child; + } + + @Nullable + @Override + public ElementPath nextFocusPath(GuiNavigationEvent event) { + return ParentElement.super.nextFocusPath(event); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + return ParentElement.super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean isFocused() { + return ParentElement.super.isFocused(); + } + + @Override + public void setFocused(boolean focused) { + ParentElement.super.setFocused(focused); + } + + @Override + public @NotNull List children() { + return Stream.concat(actionButtons.stream(), Stream.of(skinWidget, label, equipButton)).filter(Objects::nonNull).toList(); + } + + private float applyEasing(float x) { + return x * x * x; + } + + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int y = getY() + 4; + int x = getX() + 2; + if (skinWidget.isEquipped() || equipping) { + long prog; + if (Auth.getInstance().skinManagerAnimations.get()) { + if (equipping) prog = (Util.getMeasuringTimeMs() - equippingStart) / 20 % 100; + else prog = Math.abs((Util.getMeasuringTimeMs() / 30 % 200) - 100); + } else prog = 100; + var percent = (prog / 100f); + float gradientWidth; + if (equipping) { + gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); + } else { + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + applyEasing(percent) * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + } + GradientHoleRectangleRenderState.render(guiGraphics, getX() + 2, getY() + 2, getX() + getWidth() - 2, + skinWidget.getY() + skinWidget.getHeight() + 2, + gradientWidth, + equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); + } + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); + skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); + int actionButtonY = getY() + 2; + for (var button : actionButtons) { + button.setPosition(skinWidget.getX() + skinWidget.getWidth() - button.getWidth(), actionButtonY); + if (isHovered() || button.isHoveredOrFocused()) { + button.render(guiGraphics, mouseX, mouseY, partialTick); + } + actionButtonY += button.getHeight() + 2; + } + if (label != null) { + label.setPosition(x, skinWidget.getY() + skinWidget.getHeight() + 6); + label.render(guiGraphics, mouseX, mouseY, partialTick); + label.setWidth(getWidth() - 4); + equipButton.setPosition(x, label.getY() + label.getHeight() + 2); + } else { + equipButton.setPosition(x, skinWidget.getY() + skinWidget.getHeight() + 4); + } + equipButton.setWidth(getWidth() - 4); + equipButton.render(guiGraphics, mouseX, mouseY, partialTick); + + if (isHovered()) { + guiGraphics.br$outlineRect(getX(), getY(), getWidth(), getHeight(), -1); + } + } + + @Override + protected void updateNarration(NarrationMessageBuilder narrationElementOutput) { + skinWidget.appendNarrations(narrationElementOutput); + actionButtons.forEach(w -> w.appendNarrations(narrationElementOutput)); + if (label != null) { + label.appendNarrations(narrationElementOutput); + } + equipButton.appendNarrations(narrationElementOutput); + } + + private static class GradientHoleRectangleRenderState { + + public static void render(GuiGraphics graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { + var vertexConsumer = graphics.getVertexConsumers().getBuffer(RenderLayer.getGui()); + float z = 0; + //top + var pose = graphics.getMatrices().peek().getModel(); + vertexConsumer.vertex(pose, x0, y0, z).color(col1); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x1, y0, z).color(col1); + //left + vertexConsumer.vertex(pose, x0, y1, z).color(col1); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x0, y0, z).color(col1); + //bottom + vertexConsumer.vertex(pose, x1, y1, z).color(col1); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x0, y1, z).color(col1); + //right + vertexConsumer.vertex(pose, x1, y0, z).color(col1); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); + vertexConsumer.vertex(pose, x1, y1, z).color(col1); + } + } + } +} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java new file mode 100644 index 000000000..10f58a56b --- /dev/null +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; + +import com.google.common.hash.Hashing; +import com.mojang.blaze3d.texture.NativeImage; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.AxoMinecraftClient; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.util.ClientColors; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.util.Identifier; + +public class SkinManager { + + private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + + public Skin read(Path p) { + boolean slim; + String sha256; + try { + var in = Files.readAllBytes(p); + sha256 = Hashing.sha256().hashBytes(in).toString(); + try (var img = NativeImage.read(in)) { + slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + } + return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + } catch (Exception e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + } + return null; + } + + + public CompletableFuture loadSkin(Skin skin) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); + if (loadedTextures.contains(rl)) { + return CompletableFuture.completedFuture(rl); + } + + return skin.image().thenApplyAsync(bytes -> { + try { + var tex = new NativeImageBackedTexture(NativeImage.read(bytes)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((v, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load skin!", t); + } + return v; + }); + } + + public AxoIdentifier loadCape(Cape cape) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); + if (loadedTextures.contains(rl)) { + return rl; + } + + return cape.image().thenApplyAsync(bytes -> { + try { + var tex = new NativeImageBackedTexture(NativeImage.read(bytes)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((id, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load cape!", t); + } + return id; + }).getNow(null); + + } + + public void releaseAll() { + loadedTextures.forEach(id -> MinecraftClient.getInstance().getTextureManager().destroyTexture((Identifier) id)); + loadedTextures.clear(); + } + + public String getDefaultSkinHash(Account account) { + var skin = DefaultSkinHelper.getTexture(UUIDHelper.fromUndashed(account.getUuid())); + var mc = MinecraftClient.getInstance(); + var resourceManager = mc.getResourceManager(); + try { + var res = resourceManager.getResourceOrThrow(skin); + try ( + var in = res.open()) { + return Hashing.sha256().hashBytes(in.readAllBytes()).toString(); + } + } catch (IOException ignored) { + } + return null; + } +} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java new file mode 100644 index 000000000..50f2ed31a --- /dev/null +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -0,0 +1,90 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import com.mojang.blaze3d.lighting.DiffuseLighting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.model.EntityModelLayers; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Axis; +import org.jetbrains.annotations.Nullable; + +public class SkinRenderer { + private static PlayerEntityModel classicModel, slimModel; + private static final MinecraftClient minecraft = MinecraftClient.getInstance(); + + private SkinRenderer() { + } + + public static void render(GuiGraphics graphics, boolean classicVariant, + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { + if (classicModel == null && classicVariant) { + classicModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER), false); + classicModel.child = false; + } + if (slimModel == null && !classicVariant) { + slimModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER_SLIM), true); + slimModel.child = false; + } + + int width = x1 - x0; + DiffuseLighting.setupInventoryEntityLighting(); + graphics.getMatrices().push(); + graphics.getMatrices().translate(x0 + width / 2.0F, (float) (y1), 100.0F); + graphics.getMatrices().scale(scale, scale, scale); + graphics.getMatrices().translate(0.0F, -0.0625F, 0.0F); + graphics.getMatrices().rotateAround(Axis.X_POSITIVE.rotationDegrees(rotationX), 0.0F, pivotY, 0.0F); + graphics.getMatrices().multiply(Axis.Y_POSITIVE.rotationDegrees(rotationY)); + graphics.draw(); + graphics.getMatrices().push(); + graphics.getMatrices().scale(1.0F, 1.0F, -1.0F); + graphics.getMatrices().translate(0.0F, -1.5F, 0.0F); + var model = classicVariant ? classicModel : slimModel; + RenderLayer renderLayer = model.getLayer(skinTexture); + model.render(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(renderLayer), LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); + if (cape != null) { + graphics.getMatrices().translate(0.0F, 0.0F, 0.125F); + graphics.getMatrices().multiply(Axis.X_POSITIVE.rotationDegrees(6.0F)); + graphics.getMatrices().multiply(Axis.Y_POSITIVE.rotationDegrees(180.0F)); + model.renderCape(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(RenderLayer.getEntitySolid(cape)), LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV); + } + graphics.getMatrices().pop(); + graphics.draw(); + graphics.getMatrices().pop(); + DiffuseLighting.setup3DGuiLighting(); + } +} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java new file mode 100644 index 000000000..79acc96bc --- /dev/null +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -0,0 +1,147 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.ElementPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.GuiNavigationEvent; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.text.CommonTexts; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class SkinWidget extends ClickableWidget { + private static final float MODEL_HEIGHT = 2.125F; + private static final float FIT_SCALE = 0.97F; + private static final float ROTATION_SENSITIVITY = 2.5F; + private static final float DEFAULT_ROTATION_X = -5.0F; + private static final float DEFAULT_ROTATION_Y = 30.0F; + private static final float ROTATION_X_LIMIT = 50.0F; + private float rotationX = DEFAULT_ROTATION_X; + @Setter + private float rotationY = DEFAULT_ROTATION_Y; + @Getter + @Setter + private Skin skin; + @Getter + @Setter + private Cape cape; + private final Account owner; + private boolean noCape, noCapeActive; + + public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { + super(0, 0, width, height, CommonTexts.EMPTY); + this.skin = skin; + this.cape = cape; + this.owner = owner; + } + + public SkinWidget(int width, int height, Skin skin, Account owner) { + this(width, height, skin, null, owner); + } + + public void noCape(boolean noCapeActive) { + noCape = true; + this.noCapeActive = noCapeActive; + } + + @Override + protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + var minecraft = MinecraftClient.getInstance(); + + float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; + float pivotY = -1.0625F; + + AxoIdentifier skinRl; + boolean classic; + SkinManager skinManager = Auth.getInstance().getSkinManager(); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); + if (loader != null && loader.isDone()) { + skinRl = loader.join(); + classic = skin.isClassicVariant(); + } else { + var uuid = UUIDHelper.fromUndashed(owner.getUuid()); + classic = DefaultSkinHelper.getModel(uuid).equals("default"); + skinRl = DefaultSkinHelper.getTexture(uuid); + } + var capeRl = cape == null ? null : skinManager.loadCape(cape); + + SkinRenderer.render(guiGraphics, classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getX() + getWidth(), this.getY() + getHeight(), scale); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) { + this.rotationX = MathHelper.clamp(this.rotationX - (float) dragY * ROTATION_SENSITIVITY, -ROTATION_X_LIMIT, ROTATION_X_LIMIT); + this.rotationY += (float) dragX * ROTATION_SENSITIVITY; + } + + @Override + public void playDownSound(SoundManager handler) { + } + + @Override + protected void updateNarration(NarrationMessageBuilder builder) { + + } + + @Override + public @Nullable ElementPath nextFocusPath(GuiNavigationEvent event) { + return null; + } + + public boolean isEquipped() { + return noCape ? noCapeActive : (cape != null ? cape.active() : skin != null && skin.active()); + } + + public CompletableFuture equip() { + var msApi = Auth.getInstance().getMsApi(); + if (noCape) { + return msApi.hideCape(owner); + } + if (cape != null) { + return cape.equip(msApi, owner); + } + if (skin != null) { + return skin.equip(msApi, owner); + } + return msApi.resetSkin(owner); + } + + public Asset getFocusedAsset() { + return noCape ? null : cape != null ? cape : skin; + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 523b8b050..1a500b697 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -71,10 +71,10 @@ private SkinRenderer(MultiBufferSource.BufferSource bufferSource, Minecraft mine @Override protected void renderToTexture(SkinRenderState renderState, PoseStack poseStack) { - if (classicModel == null) { + if (classicModel == null && renderState.classicVariant()) { classicModel = new PlayerModel(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER), false); } - if (slimModel == null) { + if (slimModel == null && !renderState.classicVariant()) { slimModel = new PlayerModel(minecraft.getEntityModels().bakeLayer(ModelLayers.PLAYER_SLIM), true); } Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.PLAYER_SKIN); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 3059c7db2..9bf130cca 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -316,7 +316,6 @@ private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { @Override protected void clearChildren() { super.clearChildren(); - SkinRenderer.closeRenderers(); Auth.getInstance().getSkinManager().releaseAll(); } @@ -324,7 +323,6 @@ protected void clearChildren() { public void removed() { Auth.getInstance().getSkinManager().releaseAll(); Watcher.close(skinDirWatcher); - SkinRenderer.closeRenderers(); } @Override @@ -597,7 +595,7 @@ protected void updateNarration(NarrationMessageBuilder narrationElementOutput) { equipButton.appendNarrations(narrationElementOutput); } - private class GradientHoleRectangleRenderState { + private static class GradientHoleRectangleRenderState { public static void render(GuiGraphics graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { var vertexConsumer = graphics.getVertexConsumers().getBuffer(RenderLayer.getGui()); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 0c0998395..2f12705c8 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -22,9 +22,6 @@ package io.github.axolotlclient.modules.auth.skin; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import com.mojang.blaze3d.lighting.DiffuseLighting; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.GuiGraphics; @@ -34,52 +31,38 @@ import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.util.Identifier; import net.minecraft.util.math.Axis; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class SkinRenderer { - private static final Map renderers = new ConcurrentHashMap<>(); - - public static void closeRenderers() { - renderers.clear(); - } - - public static SkinRenderer getOrCreate(MinecraftClient minecraft, String id) { - return renderers.computeIfAbsent(id, _id -> new SkinRenderer(minecraft, id)); - } - - private PlayerEntityModel classicModel, slimModel; - private final MinecraftClient minecraft; - private final String id; + private static PlayerEntityModel classicModel, slimModel; + private static final MinecraftClient minecraft = MinecraftClient.getInstance(); - private SkinRenderer(MinecraftClient minecraft, String id) { - this.minecraft = minecraft; - this.id = id; + private SkinRenderer() { } - public void render(GuiGraphics graphics, boolean classicVariant, - Identifier skinTexture, - @Nullable Identifier cape, - float rotationX, - float rotationY, - float pivotY, - int x0, - int y0, - int x1, - int y1, - float scale) { - if (classicModel == null) { + public static void render(GuiGraphics graphics, boolean classicVariant, + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { + if (classicModel == null && classicVariant) { classicModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER), false); classicModel.child = false; } - if (slimModel == null) { + if (slimModel == null && !classicVariant) { slimModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER_SLIM), true); slimModel.child = false; } - int width = x1- x0; + int width = x1 - x0; graphics.getMatrices().push(); - graphics.getMatrices().translate(x0 + width / 2.0F, (float)(y1), 100.0F); + graphics.getMatrices().translate(x0 + width / 2.0F, (float) (y1), 100.0F); graphics.getMatrices().scale(scale, scale, scale); graphics.getMatrices().translate(0.0F, -0.0625F, 0.0F); graphics.getMatrices().rotateAround(Axis.X_POSITIVE.rotationDegrees(rotationX), 0.0F, pivotY, 0.0F); @@ -103,8 +86,4 @@ public void render(GuiGraphics graphics, boolean classicVariant, DiffuseLighting.setup3DGuiLighting(); graphics.getMatrices().pop(); } - - protected @NotNull String getTextureLabel() { - return "axolotlclient/skin_render/" + id; - } } diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index f776bf9d0..55f94405e 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -101,11 +101,7 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float } var capeRl = cape == null ? null : skinManager.loadCape(cape); - // You might say that using `hashCode()` like this isn't ideal, but in reality it doesn't matter. These objects get freed - // correctly by the screen so we mostly only need unique identifiers per widget which `hashCode()` provides. - var renderer = SkinRenderer.getOrCreate(minecraft, "" + hashCode()); - - renderer.render(guiGraphics, classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getXEnd(), this.getYEnd(), scale); + SkinRenderer.render(guiGraphics, classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getXEnd(), this.getYEnd(), scale); } @Override From 3837af468fb9585eb46ea2fa08b45f36955655da Mon Sep 17 00:00:00 2001 From: moehreag Date: Mon, 1 Sep 2025 00:05:01 +0200 Subject: [PATCH 08/23] port to 1.16 --- .../modules/auth/AccountsScreen.java | 20 +- .../axolotlclient/modules/auth/Auth.java | 28 +- .../auth/skin/SkinManagementScreen.java | 788 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 142 ++++ .../modules/auth/skin/SkinRenderer.java | 87 ++ .../modules/auth/skin/SkinWidget.java | 158 ++++ .../modules/auth/AccountsScreen.java | 5 +- .../axolotlclient/modules/auth/Auth.java | 15 +- .../auth/skin/SkinManagementScreen.java | 82 +- .../screenshotUtils/LoadingImageScreen.java | 8 +- .../modules/auth/AccountsScreen.java | 5 +- .../axolotlclient/modules/auth/Auth.java | 14 +- .../auth/skin/SkinManagementScreen.java | 42 +- .../modules/auth/AccountsScreen.java | 5 +- .../axolotlclient/modules/auth/Auth.java | 15 +- .../auth/skin/SkinManagementScreen.java | 43 +- .../modules/auth/AccountsScreen.java | 5 +- .../axolotlclient/modules/auth/Auth.java | 15 +- .../axolotlclient/modules/auth/Account.java | 6 +- .../axolotlclient/modules/auth/Accounts.java | 3 +- .../axolotlclient/modules/auth/MSApi.java | 126 +-- .../assets/axolotlclient/lang/en_us.json | 1 + 22 files changed, 1430 insertions(+), 183 deletions(-) create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 02cfbaa42..081ae3749 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth; +import io.github.axolotlclient.modules.auth.skin.SkinManagementScreen; import net.minecraft.client.gui.screen.ConfirmScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.ScreenTexts; @@ -36,6 +37,7 @@ public class AccountsScreen extends Screen { private ButtonWidget loginButton; private ButtonWidget deleteButton; private ButtonWidget refreshButton; + private ButtonWidget skinsButton; public AccountsScreen(Screen currentScreen) { super(new TranslatableText("accounts")); @@ -77,10 +79,14 @@ public void init() { accountsListWidget.setAccounts(Auth.getInstance().getAccounts()); - addButton(loginButton = new ButtonWidget(this.width / 2 - 154, this.height - 52, 150, 20, new TranslatableText("auth.login"), + addButton(loginButton = new ButtonWidget(this.width / 2 - 154, this.height - 52, 100, 20, new TranslatableText("auth.login"), buttonWidget -> login())); - this.addButton(new ButtonWidget(this.width / 2 + 4, this.height - 52, 150, 20, new TranslatableText("auth.add"), + addButton(skinsButton = new ButtonWidget(this.width / 2 - 50, this.height - 52, 100, 20, new TranslatableText("skins.manage"), + btn -> client.openScreen(new SkinManagementScreen( + this, accountsListWidget.getSelected().getAccount())))); + + this.addButton(new ButtonWidget(this.width / 2 + 4 + 50, this.height - 52, 100, 20, new TranslatableText("auth.add"), button -> { if (!Auth.getInstance().allowOfflineAccounts()) { initMSAuth(); @@ -127,10 +133,7 @@ private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { - Auth.getInstance().save(); - refresh(); - })); + entry.getAccount().refresh(Auth.getInstance().getMsApi()); } } @@ -138,9 +141,10 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (client.world == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = true; + refreshButton.active = skinsButton.active = !entry.getAccount().isOffline(); + deleteButton.active = true; } else { - loginButton.active = deleteButton.active = refreshButton.active = false; + loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } } diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 9d985180d..0298c9de5 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -23,6 +23,7 @@ package io.github.axolotlclient.modules.auth; import java.util.*; +import java.util.concurrent.CompletableFuture; import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; @@ -33,13 +34,13 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.mixin.MinecraftClientAccessor; import io.github.axolotlclient.modules.Module; +import io.github.axolotlclient.modules.auth.skin.SkinManager; import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.notifications.Notifications; import io.github.axolotlclient.util.options.GenericOption; import lombok.Getter; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.ConfirmScreen; -import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.util.DefaultSkinHelper; import net.minecraft.client.util.Session; import net.minecraft.text.TranslatableText; @@ -50,12 +51,15 @@ public class Auth extends Accounts implements Module { @Getter private final static Auth Instance = new Auth(); public final BooleanOption showButton = new BooleanOption("auth.showButton", false); + public final BooleanOption skinManagerAnimations = new BooleanOption("skins.manage.animations", true); private final MinecraftClient client = MinecraftClient.getInstance(); private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> client.openScreen(new AccountsScreen(client.currentScreen))); private final Map textures = new HashMap<>(); private final Set loadingTexture = new HashSet<>(); private final Map profileCache = new WeakHashMap<>(); + @Getter + private final SkinManager skinManager = new SkinManager(); @Override public void init() { @@ -86,12 +90,10 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(new TranslatableText("auth.notif.title"), new TranslatableText("auth.notif.refreshing", account.getName())); } - account.refresh(msApi).thenAccept(res -> { - res.ifPresent(a -> { - if (!a.isExpired()) { - login(a); - } - }); + account.refresh(msApi).thenAccept(a -> { + if (!a.isExpired()) { + login(a); + } }).thenRun(this::save); } else { try { @@ -136,14 +138,18 @@ public void loadTextures(String uuid, String name) { } @Override - void showAccountsExpiredScreen(Account account) { - Screen current = client.currentScreen; + CompletableFuture showAccountsExpiredScreen(Account account) { + var screen = client.currentScreen; + var fut = new CompletableFuture(); client.execute(() -> client.openScreen(new ConfirmScreen((bl) -> { - client.openScreen(current); if (bl) { - msApi.startDeviceAuth(); + msApi.startDeviceAuth().thenRun(() -> fut.complete(account)); + } else { + fut.cancel(true); } + client.openScreen(screen); }, new TranslatableText("auth"), new TranslatableText("auth.accountExpiredNotice", account.getName())))); + return fut; } @Override diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java new file mode 100644 index 000000000..423e9701c --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -0,0 +1,788 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.mojang.blaze3d.systems.RenderSystem; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.modules.hud.util.DrawUtil; +import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.Watcher; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.ParentElement; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ScreenTexts; +import net.minecraft.client.gui.widget.AbstractButtonWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinManagementScreen extends Screen { + private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); + private static final int LIST_SKIN_WIDTH = 75; + private static final int LIST_SKIN_HEIGHT = 110; + private static final Text TEXT_EQUIPPING = new TranslatableText("skins.manage.equipping"); + private final Screen parent; + private final Account account; + private MSApi.MCProfile cachedProfile; + private SkinListWidget skinList; + private SkinListWidget capesList; + private boolean capesTab; + private SkinWidget current; + private final Watcher skinDirWatcher; + private final List drawables = new ArrayList<>(); + private boolean triedAccountRefresh; + + public SkinManagementScreen(Screen parent, Account account) { + super(new TranslatableText("skins.manage")); + this.parent = parent; + this.account = account; + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + } + + @Override + public void init() { + int headerHeight = 33; + int contentHeight = height - headerHeight * 2; + + var back = addDrawableChild(new ButtonWidget(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20, ScreenTexts.BACK, btn -> onClose())); + + var loadingPlaceholder = new AbstractButtonWidget(0, headerHeight, width, contentHeight, new TranslatableText("skins.loading")) { + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + int centerX = this.x + this.getWidth() / 2; + int centerY = this.y + this.getHeight() / 2; + Text text = this.getMessage(); + textRenderer.draw(graphics, text, centerX - textRenderer.getWidth(text) / 2f, centerY - 9, -1); + String string = switch ((int) (Util.getMeasuringTimeMs() / 300L % 4L)) { + case 1, 3 -> "o O o"; + case 2 -> "o o O"; + default -> "O o o"; + }; + textRenderer.draw(graphics, string, centerX - textRenderer.getWidth(string) / 2f, centerY + 9, 0xFF808080); + } + + @Override + protected MutableText getNarrationMessage() { + return LiteralText.EMPTY.copy(); + } + }; + loadingPlaceholder.active = false; + if (!triedAccountRefresh) { + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + } + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); + skinList.setLeftPos(width / 2); + capesList.setLeftPos(width / 2); + var currentHeight = Math.min((width / 2f) * 120 / 85, contentHeight); + var currentWidth = currentHeight * 85 / 120; + current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); + current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); + + if (!capesTab) { + capesList.visible = capesList.active = false; + } else { + skinList.visible = skinList.active = false; + } + List navBar = new ArrayList<>(); + var skinsTab = new ButtonWidget(width * 3 / 4 - 102, headerHeight, 100, 20, new TranslatableText("skins.nav.skins"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = true; + capesList.visible = capesList.active = false; + capesTab = false; + }); + navBar.add(skinsTab); + var capesTab = new ButtonWidget(width * 3 / 4 + 2, headerHeight, 100, 20, new TranslatableText("skins.nav.capes"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = false; + capesList.visible = capesList.active = true; + this.capesTab = true; + }); + navBar.add(capesTab); + skinsTab.active = this.capesTab; + capesTab.active = !this.capesTab; + Runnable addWidgets = () -> { + clear(); + addDrawableChild(current); + addDrawableChild(skinList); + addDrawableChild(capesList); + addDrawableChild(skinsTab); + addDrawableChild(capesTab); + addDrawableChild(back); + }; + if (cachedProfile != null) { + initDisplay(); + addWidgets.run(); + return; + } + CompletableFuture fut; + if (account.needsRefresh()) { + if (triedAccountRefresh) { + fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { + }); + } else { + triedAccountRefresh = true; + account.refresh(Auth.getInstance().getMsApi()); + return; + } + } else { + fut = CompletableFuture.completedFuture(null); + } + fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAcceptAsync(profile -> { + cachedProfile = profile; + initDisplay(); + addWidgets.run(); + }).exceptionally(t -> { + if (!triedAccountRefresh) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + } + var error = new TranslatableText("skins.error.failed_to_load"); + var errorDesc = new TranslatableText(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + clear(); + addDrawableChild(back); + class TextWidget extends AbstractButtonWidget { + + public TextWidget(int x, int y, int width, int height, Text message, TextRenderer textRenderer) { + super(x, y, width, height, message); + } + + @Override + public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { + textRenderer.draw(matrices, getMessage(), x, y, -1); + } + } + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc, textRenderer)); + return null; + }); + } + + private T addDrawableChild(T child) { + drawables.add(child); + return addChild(child); + } + + private void clear() { + children.clear(); + buttons.clear(); + drawables.clear(); + } + + @Override + public void render(MatrixStack graphics, int mouseX, int mouseY, float delta) { + renderBackground(graphics); + drawables.forEach(d -> d.render(graphics, mouseX, mouseY, delta)); + drawCenteredText(graphics, textRenderer, getTitle(), width / 2, 33 / 2 - textRenderer.fontHeight / 2, -1); + } + + private void initDisplay() { + loadSkinsList(); + loadCapesList(); + } + + private void refreshCurrentList() { + if (capesTab) { + var scroll = capesList.getScrollAmount(); + loadCapesList(); + capesList.setScrollAmount(scroll); + } else { + var scroll = skinList.getScrollAmount(); + loadSkinsList(); + skinList.setScrollAmount(scroll); + } + } + + private void loadCapesList() { + capesList.clearEntries0(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + var capes = profile.capes(); + var deselectCape = createWidgetForCape(current.getSkin(), null); + var activeCape = capes.stream().filter(Cape::active).findFirst(); + current.setCape(activeCape.orElse(null)); + deselectCape.noCape(activeCape.isEmpty()); + for (int i = 0; i < capes.size() + 1; i += columns) { + Entry widget; + if (i == 0) { + widget = createEntry(capesList.getEntryContentsHeight(), deselectCape, new TranslatableText("skins.capes.no_cape")); + } else { + var cape = capes.get(i - 1); + widget = createEntryForCape(current.getSkin(), cape, capesList.getEntryContentsHeight()); + } + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < capes.size() + 1 - c)) continue; + var cape2 = capes.get(i + c - 1); + Entry widget2 = createEntryForCape(current.getSkin(), cape2, capesList.getEntryContentsHeight()); + + widgets.add(widget2); + } + capesList.addEntry(new Row(widgets)); + } + } + + private void loadSkinsList() { + skinList.clearEntries0(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + List skins = new ArrayList<>(profile.skins()); + var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); + var local = new ArrayList<>(loadLocalSkins()); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + skins.replaceAll(s -> { + if (s instanceof MSApi.MCProfile.OnlineSkin online) { + if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { + local.remove(localHashes.remove(s.textureKey())); + return new Skin.Shared(file, online); + } + } + return s; + }); + skins.addAll(local); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } + populateSkinList(skins, columns); + } + + private List loadLocalSkins() { + try { + Files.createDirectories(SKINS_DIR); + try (Stream skins = Files.list(SKINS_DIR)) { + return skins.filter(Files::isRegularFile).sorted(Comparator.comparingLong(p -> { + try { + return Files.getLastModifiedTime(p).toMillis(); + } catch (IOException e) { + return 0L; + } + }).reversed()).map(Auth.getInstance().getSkinManager()::read).filter(Objects::nonNull).toList(); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to read skins dir!", e); + } + return Collections.emptyList(); + } + + private void populateSkinList(List skins, int columns) { + int entryHeight = skinList.getEntryContentsHeight(); + for (int i = 0; i < skins.size(); i += columns) { + var s = skins.get(i); + if (s != null && s.active()) { + current.setSkin(s); + } + var widget = createEntryForSkin(s, entryHeight); + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < skins.size() - c)) continue; + var s2 = skins.get(i + c); + if (s2 != null && s2.active()) { + current.setSkin(s2); + } + var widget2 = createEntryForSkin(s2, entryHeight); + widgets.add(widget2); + } + skinList.addEntry(new Row(widgets)); + } + } + + @Override + public void filesDragged(List packs) { + packs.forEach(p -> { + try { + Files.copy(p, SKINS_DIR.resolve(p.getFileName())); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }); + loadSkinsList(); + } + + private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); + } + + private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { + return createEntry(entryHeight, createWidgetForCape(currentSkin, cape), new LiteralText(cape.alias())); + } + + private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account); + widget2.setRotationY(210); + return widget2; + } + + @Override + public void resize(MinecraftClient client, int width, int height) { + Auth.getInstance().getSkinManager().releaseAll(); + super.resize(client, width, height); + } + + @Override + public void removed() { + Auth.getInstance().getSkinManager().releaseAll(); + Watcher.close(skinDirWatcher); + } + + @Override + public void onClose() { + client.openScreen(parent); + } + + private SkinListWidget getCurrentList() { + return capesTab ? capesList : skinList; + } + + private class SkinListWidget extends ElementListWidget { + public boolean active = true, visible = true; + + public SkinListWidget(MinecraftClient minecraft, int width, int height, int y, int entryHeight) { + super(minecraft, width, SkinManagementScreen.this.height, y, y + height, entryHeight); + setRenderHeader(false, 0); + //setRenderBackground(false); + } + + @Override + public int addEntry(Row entry) { + return super.addEntry(entry); + } + + @Override + protected int getScrollbarPositionX() { + return right - 8; + } + + @Override + public int getRowLeft() { + return left + 3; + } + + @Override + public int getRowWidth() { + if (!(getMaxScroll() > 0)) { + return width - 4; + } + return width - 14; + } + + private int getMaxScroll() { + return Math.max(0, this.getMaxPosition() - (this.bottom - this.top - 4)); + } + + public int getEntryContentsHeight() { + return itemHeight - 4; + } + + public void clearEntries0() { + super.clearEntries(); + } + + @Override + public void centerScrollOn(Row entry) { + super.centerScrollOn(entry); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amountY) { + if (!visible) return false; + return super.mouseScrolled(mouseX, mouseY, amountY); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return active && visible && super.isMouseOver(mouseX, mouseY); + } + + @Override + public void render(MatrixStack graphics, int mouseX, int mouseY, float delta) { + if (!visible) return; + super.render(graphics, mouseX, mouseY, delta); + } + } + + private class Row extends ElementListWidget.Entry { + private final List widgets; + + public Row(List entries) { + this.widgets = entries; + } + + @Override + public void render(MatrixStack guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + int x = left; + if (widgets.isEmpty()) return; + int count = widgets.size(); + int padding = ((width - 5 * (count - 1)) / count); + for (var w : widgets) { + w.x = x; + w.y = top; + w.setWidth(padding); + w.render(guiGraphics, mouseX, mouseY, partialTick); + x += w.getWidth() + 5; + } + } + + @Override + public @NotNull List children() { + return widgets; + } + + @Override + public void setFocused(@Nullable Element focused) { + super.setFocused(focused); + if (focused != null) { + getCurrentList().centerScrollOn(this); + } + } + } + + Entry createEntry(int height, SkinWidget widget) { + return createEntry(height, widget, null); + } + + Entry createEntry(int height, SkinWidget widget, Text label) { + return new Entry(height, widget, label); + } + + private class Entry extends AbstractButtonWidget implements ParentElement { + private final SkinWidget skinWidget; + private final @Nullable AbstractButtonWidget label; + private final List actionButtons = new ArrayList<>(); + private final AbstractButtonWidget equipButton; + private boolean equipping; + private long equippingStart; + @Nullable + private Element focused; + private boolean dragging; + + public Entry(int height, SkinWidget widget, @Nullable Text label) { + super(0, 0, widget.getWidth(), height, LiteralText.EMPTY); + widget.setWidth(getWidth() - 4); + var asset = widget.getFocusedAsset(); + if (asset != null) { + if (asset.isLocal()) { + var delete = new ButtonWidget(0, 0, 11, 11, LiteralText.EMPTY, btn -> { + btn.active = false; + client.openScreen(new ConfirmScreen(confirmed -> { + client.openScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); + } + } + btn.active = true; + }, new TranslatableText("skins.manage.delete.confirm"), (Text) (asset.active() ? + new TranslatableText("skins.manage.delete.confirm.desc_active") : + new TranslatableText("skins.manage.delete.confirm.desc") + ).br$color(Colors.RED.toInt()))); + }) { + + private final Text tooltip = new TranslatableText("skins.manage.delete"); + private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); + + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + super.renderButton(graphics, mouseX, mouseY, delta); + client.getTextureManager().bindTexture(sprite); + drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { + renderTooltip(matrices, tooltip, mouseX, mouseY); + } + }; + this.actionButtons.add(delete); + } + if (asset.supportsDownload() && !asset.isLocal()) { + var download = new ButtonWidget(0, 0, 11, 11, LiteralText.EMPTY, btn -> { + btn.active = false; + asset.image().thenAcceptAsync(b -> { + try { + var out = SKINS_DIR.resolve(asset.textureKey()); + Files.createDirectories(out.getParent()); + Files.write(out, b); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); + } + refreshCurrentList(); + btn.active = true; + }); + }) { + private final Text tooltip = new TranslatableText("skins.manage.download"); + private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + super.renderButton(graphics, mouseX, mouseY, delta); + client.getTextureManager().bindTexture(sprite); + drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { + renderTooltip(matrices, tooltip, mouseX, mouseY); + } + }; + this.actionButtons.add(download); + } + } + if (label != null) { + this.label = new AbstractButtonWidget(0, 0, widget.getWidth(), 16, label) { + @Override + public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) { + DrawUtil.drawScrollableText(guiGraphics, textRenderer, getMessage(), x + 2, y, x + width - 2, y + height, -1); + } + }; + this.label.active = false; + } else { + this.label = null; + } + this.equipButton = new ButtonWidget(0, 0, widget.getWidth(), 20, new TranslatableText( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> { + equippingStart = Util.getMeasuringTimeMs(); + equipping = true; + btn.setMessage(TEXT_EQUIPPING); + btn.active = false; + widget.equip().thenAcceptAsync(p -> { + cachedProfile = p; + refreshCurrentList(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + return null; + }); + }); + this.equipButton.active = !widget.isEquipped(); + this.skinWidget = widget; + } + + @Override + public final boolean isDragging() { + return this.dragging; + } + + @Override + public final void setDragging(boolean dragging) { + this.dragging = dragging; + } + + @Nullable + @Override + public Element getFocused() { + return this.focused; + } + + @Override + public void setFocused(@Nullable Element child) { + /*if (this.focused != null) { + this.focused.setFocused(false); + } + + if (child != null) { + child.setFocused(true); + }*/ + + this.focused = child; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + return ParentElement.super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean isFocused() { + return getFocused() != null; + } + + @Override + public void setFocused(boolean focused) { + + } + + @Override + public @NotNull List children() { + return Stream.concat(actionButtons.stream(), Stream.of(skinWidget, label, equipButton)).filter(Objects::nonNull).toList(); + } + + private float applyEasing(float x) { + return x * x * x; + } + + @Override + public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) { + int y = this.y + 4; + int x = this.x + 2; + if (skinWidget.isEquipped() || equipping) { + long prog; + if (Auth.getInstance().skinManagerAnimations.get()) { + if (equipping) prog = (Util.getMeasuringTimeMs() - equippingStart) / 20 % 100; + else prog = Math.abs((Util.getMeasuringTimeMs() / 30 % 200) - 100); + } else prog = 100; + var percent = (prog / 100f); + float gradientWidth; + if (equipping) { + gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); + } else { + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + applyEasing(percent) * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + } + GradientHoleRectangleRenderState.render(guiGraphics, this.x + 2, this.y + 2, this.x + getWidth() - 2, + skinWidget.getY() + skinWidget.getHeight() + 2, + gradientWidth, + equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); + } + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); + skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); + int actionButtonY = this.y + 2; + for (var button : actionButtons) { + button.x = skinWidget.getX() + skinWidget.getWidth() - button.getWidth(); + button.y = actionButtonY; + if (isHovered() || button.isHovered()) { + button.render(guiGraphics, mouseX, mouseY, partialTick); + } + actionButtonY += button.getHeight() + 2; + } + if (label != null) { + label.x = x; + label.y = skinWidget.getY() + skinWidget.getHeight() + 6; + label.render(guiGraphics, mouseX, mouseY, partialTick); + label.setWidth(getWidth() - 4); + equipButton.x = x; + equipButton.y = label.y + label.getHeight() + 2; + } else { + equipButton.x = x; + equipButton.y = skinWidget.getY() + skinWidget.getHeight() + 4; + } + equipButton.setWidth(getWidth() - 4); + equipButton.render(guiGraphics, mouseX, mouseY, partialTick); + + if (isHovered()) { + guiGraphics.br$outlineRect(this.x, this.y, getWidth(), getHeight(), -1); + } + } + + /*@Override + protected void updateNarration(NarrationMessageBuilder narrationElementOutput) { + skinWidget.appendNarrations(narrationElementOutput); + actionButtons.forEach(w -> w.appendNarrations(narrationElementOutput)); + if (label != null) { + label.appendNarrations(narrationElementOutput); + } + equipButton.appendNarrations(narrationElementOutput); + }*/ + + private static class GradientHoleRectangleRenderState { + + public static void render(MatrixStack graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { + RenderSystem.disableTexture(); + RenderSystem.enableBlend(); + RenderSystem.disableAlphaTest(); + RenderSystem.defaultBlendFunc(); + RenderSystem.shadeModel(7425); + var tessellator = Tessellator.getInstance(); + var vertexConsumer = tessellator.getBuffer(); + float z = 0; + //top + var pose = graphics.peek().getModel(); + vertexConsumer.begin(7, VertexFormats.POSITION_COLOR); + vertexConsumer.vertex(pose, x0, y0, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1, y0, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + //left + vertexConsumer.vertex(pose, x0, y1, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0, y0, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + //bottom + vertexConsumer.vertex(pose, x1, y1, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x0, y1, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + //right + vertexConsumer.vertex(pose, x1, y0, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2 >> 16 & 255, col2 >> 8 & 255, col2 & 255, col2 >> 24 & 255).next(); + vertexConsumer.vertex(pose, x1, y1, z).color(col1 >> 16 & 255, col1 >> 8 & 255, col1 & 255, col1 >> 24 & 255).next(); + tessellator.draw(); + RenderSystem.shadeModel(7424); + RenderSystem.disableBlend(); + RenderSystem.enableAlphaTest(); + RenderSystem.enableTexture(); + } + } + } +} diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java new file mode 100644 index 000000000..f862a1bbb --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -0,0 +1,142 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; + +import com.google.common.hash.Hashing; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.AxoMinecraftClient; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.util.ClientColors; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.util.Identifier; +import org.lwjgl.system.MemoryStack; + +public class SkinManager { + + private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + + public Skin read(Path p) { + boolean slim; + String sha256; + try { + var in = Files.readAllBytes(p); + sha256 = Hashing.sha256().hashBytes(in).toString(); + try (var img = NativeImage.read(ByteBuffer.wrap(in))) { + slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + } + return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + } catch (Exception e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + } + return null; + } + + + public CompletableFuture loadSkin(Skin skin) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); + if (loadedTextures.contains(rl)) { + return CompletableFuture.completedFuture(rl); + } + + return skin.image().thenApplyAsync(bytes -> { + try (MemoryStack memoryStack = MemoryStack.stackPush()) { + ByteBuffer byteBuffer = memoryStack.malloc(bytes.length); + byteBuffer.put(bytes); + byteBuffer.rewind(); + var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((v, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load skin!", t); + } + return v; + }); + } + + public AxoIdentifier loadCape(Cape cape) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); + if (loadedTextures.contains(rl)) { + return rl; + } + + return cape.image().thenApplyAsync(bytes -> { + try (MemoryStack memoryStack = MemoryStack.stackPush()) { + ByteBuffer byteBuffer = memoryStack.malloc(bytes.length); + byteBuffer.put(bytes); + byteBuffer.rewind(); + var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer)); + MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((id, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load cape!", t); + } + return id; + }).getNow(null); + + } + + public void releaseAll() { + loadedTextures.forEach(id -> MinecraftClient.getInstance().getTextureManager().destroyTexture((Identifier) id)); + loadedTextures.clear(); + } + + public String getDefaultSkinHash(Account account) { + var skin = DefaultSkinHelper.getTexture(UUIDHelper.fromUndashed(account.getUuid())); + var mc = MinecraftClient.getInstance(); + var resourceManager = mc.getResourceManager(); + try { + var res = resourceManager.getResource(skin); + try ( + var in = res.br$asStream()) { + return Hashing.sha256().hashBytes(in.readAllBytes()).toString(); + } + } catch (IOException ignored) { + } + return null; + } +} diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java new file mode 100644 index 000000000..c4be27ad7 --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -0,0 +1,87 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import net.minecraft.client.render.*; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.util.math.Vector3f; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public class SkinRenderer { + private static PlayerEntityModel classicModel, slimModel; + + private SkinRenderer() { + } + + public static void render(MatrixStack graphics, boolean classicVariant, + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { + if (classicModel == null && classicVariant) { + classicModel = new PlayerEntityModel<>(0, false); + classicModel.child = false; + } + if (slimModel == null && !classicVariant) { + slimModel = new PlayerEntityModel<>(0, true); + slimModel.child = false; + } + + int width = x1 - x0; + DiffuseLighting.disable(); + graphics.push(); + graphics.translate(x0 + width / 2.0F, (float) (y1), 100.0F); + graphics.scale(scale, scale, scale); + graphics.translate(0.0F, -0.0625F, 0.0F); + graphics.translate(0, pivotY, 0); + graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(rotationX)); + graphics.translate(0, -pivotY, 0); + graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(rotationY)); + graphics.push(); + graphics.scale(1.0F, 1.0F, -1.0F); + graphics.translate(0.0F, -1.5F, 0.0F); + var model = classicVariant ? classicModel : slimModel; + RenderLayer renderLayer = model.getLayer(skinTexture); + var tessellator = Tessellator.getInstance(); + var buf = VertexConsumerProvider.immediate(tessellator.getBuffer()); + model.render(graphics, buf.getBuffer(renderLayer), 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); + if (cape != null) { + graphics.translate(0.0F, 0.0F, 0.125F); + graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F)); + graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F)); + model.renderCape(graphics, buf.getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); + } + graphics.pop(); + tessellator.draw(); + graphics.pop(); + DiffuseLighting.enableGuiDepthLighting(); + } +} diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java new file mode 100644 index 000000000..2efd72a66 --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -0,0 +1,158 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.widget.AbstractButtonWidget; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.MutableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class SkinWidget extends AbstractButtonWidget { + private static final float MODEL_HEIGHT = 2.125F; + private static final float FIT_SCALE = 0.97F; + private static final float ROTATION_SENSITIVITY = 2.5F; + private static final float DEFAULT_ROTATION_X = -5.0F; + private static final float DEFAULT_ROTATION_Y = 30.0F; + private static final float ROTATION_X_LIMIT = 50.0F; + private float rotationX = DEFAULT_ROTATION_X; + @Setter + private float rotationY = DEFAULT_ROTATION_Y; + @Getter + @Setter + private Skin skin; + @Getter + @Setter + private Cape cape; + private final Account owner; + private boolean noCape, noCapeActive; + + public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { + super(0, 0, width, height, LiteralText.EMPTY); + this.skin = skin; + this.cape = cape; + this.owner = owner; + } + + public SkinWidget(int width, int height, Skin skin, Account owner) { + this(width, height, skin, null, owner); + } + + public void noCape(boolean noCapeActive) { + noCape = true; + this.noCapeActive = noCapeActive; + } + + @Override + public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) { + var minecraft = MinecraftClient.getInstance(); + + float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; + float pivotY = -1.0625F; + + AxoIdentifier skinRl; + boolean classic; + SkinManager skinManager = Auth.getInstance().getSkinManager(); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); + if (loader != null && loader.isDone()) { + skinRl = loader.join(); + classic = skin.isClassicVariant(); + } else { + var uuid = UUIDHelper.fromUndashed(owner.getUuid()); + classic = DefaultSkinHelper.getModel(uuid).equals("default"); + skinRl = DefaultSkinHelper.getTexture(uuid); + } + var capeRl = cape == null ? null : skinManager.loadCape(cape); + + SkinRenderer.render(guiGraphics, classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getX() + getWidth(), this.getY() + getHeight(), scale); + } + + public int getY() { + return y; + } + + public int getX() { + return x; + } + + @Override + protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) { + this.rotationX = MathHelper.clamp(this.rotationX - (float) dragY * ROTATION_SENSITIVITY, -ROTATION_X_LIMIT, ROTATION_X_LIMIT); + this.rotationY += (float) dragX * ROTATION_SENSITIVITY; + } + + @Override + public void playDownSound(SoundManager handler) { + } + + @Override + protected MutableText getNarrationMessage() { + return LiteralText.EMPTY.copy(); + } + + @Override + public boolean changeFocus(boolean lookForwards) { + return false; + } + + public boolean isEquipped() { + return noCape ? noCapeActive : (cape != null ? cape.active() : skin != null && skin.active()); + } + + public CompletableFuture equip() { + var msApi = Auth.getInstance().getMsApi(); + if (noCape) { + return msApi.hideCape(owner); + } + if (cape != null) { + return cape.equip(msApi, owner); + } + if (skin != null) { + return skin.equip(msApi, owner); + } + return msApi.resetSkin(owner); + } + + public Asset getFocusedAsset() { + return noCape ? null : cape != null ? cape : skin; + } + + public void setPosition(int x, int y) { + this.x = x; + this.y = y; + } +} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 15f0dff53..6730ce829 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -139,10 +139,7 @@ private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { - Auth.getInstance().save(); - refresh(); - })); + entry.getAccount().refresh(Auth.getInstance().getMsApi()); } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index c59c5845e..002cd25a3 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -23,6 +23,7 @@ package io.github.axolotlclient.modules.auth; import java.util.*; +import java.util.concurrent.CompletableFuture; import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; @@ -94,11 +95,11 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Text.translatable("auth.notif.title"), Text.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(a -> { if (!a.isExpired()) { login(a); } - })).thenRun(this::save); + }).thenRun(this::save); } else { try { API.getInstance().shutdown(); @@ -126,14 +127,18 @@ protected void login(Account account) { } @Override - void showAccountsExpiredScreen(Account account) { + CompletableFuture showAccountsExpiredScreen(Account account) { Screen current = client.currentScreen; + var fut = new CompletableFuture(); client.execute(() -> client.setScreen(new ConfirmScreen((bl) -> { - client.setScreen(current); if (bl) { - msApi.startDeviceAuth(); + msApi.startDeviceAuth().thenRun(() -> fut.complete(account)); + } else { + fut.cancel(true); } + client.setScreen(current); }, Text.translatable("auth"), Text.translatable("auth.accountExpiredNotice", account.getName())))); + return fut; } @Override diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index b2de4f0c0..40ae65299 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -71,6 +71,7 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; + private boolean triedAccountRefresh; public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); @@ -84,7 +85,10 @@ protected void init() { int headerHeight = 33; int contentHeight = height - headerHeight * 2; - addDrawableChild(new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle(), textRenderer)); + var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle(), textRenderer); + if (!triedAccountRefresh) { + addDrawableChild(titleWidget); + } var back = addDrawableChild(ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); @@ -105,8 +109,10 @@ protected void updateNarration(NarrationMessageBuilder builder) { } }; loadingPlaceholder.active = false; - addDrawableChild(loadingPlaceholder); - addDrawableChild(back); + if (!triedAccountRefresh) { + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + } skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setLeftPos(width / 2); @@ -145,8 +151,8 @@ protected void updateNarration(NarrationMessageBuilder builder) { skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { - remove(back); - remove(loadingPlaceholder); + clearChildren(); + addDrawableChild(titleWidget); addDrawableChild(current); addDrawableChild(skinList); addDrawableChild(capesList); @@ -161,7 +167,14 @@ protected void updateNarration(NarrationMessageBuilder builder) { } CompletableFuture fut; if (account.needsRefresh()) { - fut = account.refresh(Auth.getInstance().getMsApi()); + if (triedAccountRefresh) { + fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { + }); + } else { + triedAccountRefresh = true; + account.refresh(Auth.getInstance().getMsApi()); + return; + } } else { fut = CompletableFuture.completedFuture(null); } @@ -171,12 +184,16 @@ protected void updateNarration(NarrationMessageBuilder builder) { initDisplay(); addWidgets.run(); }).exceptionally(t -> { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (!triedAccountRefresh) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + } var error = Text.translatable("skins.error.failed_to_load"); - var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); - remove(loadingPlaceholder); + var errorDesc = Text.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + clearChildren(); + addDrawableChild(titleWidget); addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc, textRenderer)); + addDrawableChild(back); return null; }); } @@ -252,7 +269,6 @@ private void loadSkinsList() { } return s; }); - //local.removeIf(s -> hashes.contains(s.textureKey())); skins.addAll(local); if (!hashes.contains(defaultSkinHash)) { skins.add(null); @@ -328,9 +344,9 @@ private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { } @Override - protected void clearChildren() { - super.clearChildren(); + protected void clearAndInit() { Auth.getInstance().getSkinManager().releaseAll(); + super.clearAndInit(); } @Override @@ -496,17 +512,17 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { } } btn.active = true; - }, Text.translatable("skins.manage.delete.confirm"), asset.active() ? + }, Text.translatable("skins.manage.delete.confirm"), (Text) (asset.active() ? Text.translatable("skins.manage.delete.confirm.desc_active") : - (Text) Text.translatable("skins.manage.delete.confirm.desc") - .br$color(Colors.RED.toInt()))); + Text.translatable("skins.manage.delete.confirm.desc") + ).br$color(Colors.RED.toInt()))); }, Supplier::get) { private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); @Override protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(SPRITE, getX(), getY(), 0, 0, 7, 7, 7, 7); + graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); } @Override @@ -537,7 +553,7 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int @Override protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(SPRITE, getX(), getY(), 0, 0, 7, 7, 7, 7); + graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); } @Override @@ -714,25 +730,25 @@ public static void render(GuiGraphics graphics, int x0, int y0, int x1, int y1, float z = 0; //top var pose = graphics.getMatrices().peek().getModel(); - vertexConsumer.vertex(pose, x0, y0, z).color(col1); - vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x1, y0, z).color(col1); + vertexConsumer.vertex(pose, x0, y0, z).color(col1).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x1, y0, z).color(col1).next(); //left - vertexConsumer.vertex(pose, x0, y1, z).color(col1); - vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x0, y0, z).color(col1); + vertexConsumer.vertex(pose, x0, y1, z).color(col1).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y0 + gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x0, y0, z).color(col1).next(); //bottom - vertexConsumer.vertex(pose, x1, y1, z).color(col1); - vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x0, y1, z).color(col1); + vertexConsumer.vertex(pose, x1, y1, z).color(col1).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x0 + gradientWidth, y1 - gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x0, y1, z).color(col1).next(); //right - vertexConsumer.vertex(pose, x1, y0, z).color(col1); - vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2); - vertexConsumer.vertex(pose, x1, y1, z).color(col1); + vertexConsumer.vertex(pose, x1, y0, z).color(col1).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y0 + gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x1 - gradientWidth, y1 - gradientWidth, z).color(col2).next(); + vertexConsumer.vertex(pose, x1, y1, z).color(col1).next(); } } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/screenshotUtils/LoadingImageScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/screenshotUtils/LoadingImageScreen.java index 9b88f618b..cbf337a26 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/screenshotUtils/LoadingImageScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/screenshotUtils/LoadingImageScreen.java @@ -87,10 +87,10 @@ public void closeScreen() { private void drawHorizontalGradient(GuiGraphics guiGraphics, int x1, int y1, int y2, int x2) { VertexConsumer consumer = client.getBufferBuilders().getEntityVertexConsumers().getBuffer(RenderLayer.getGui()); Matrix4f matrix4f = guiGraphics.getMatrices().peek().getModel(); - consumer.vertex(matrix4f, x1, y1, 0).color(LoadingImageScreen.bgColor); - consumer.vertex(matrix4f, x1, y2, 0).color(LoadingImageScreen.bgColor); - consumer.vertex(matrix4f, x2, y2, 0).color(LoadingImageScreen.accent); - consumer.vertex(matrix4f, x2, y1, 0).color(LoadingImageScreen.accent); + consumer.vertex(matrix4f, x1, y1, 0).color(LoadingImageScreen.bgColor).next(); + consumer.vertex(matrix4f, x1, y2, 0).color(LoadingImageScreen.bgColor).next(); + consumer.vertex(matrix4f, x2, y2, 0).color(LoadingImageScreen.accent).next(); + consumer.vertex(matrix4f, x2, y1, 0).color(LoadingImageScreen.accent).next(); } private double easeInOutCubic(double x) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 70ca489a6..38ca87b9a 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -130,10 +130,7 @@ private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelected(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> minecraft.execute(() -> { - Auth.getInstance().save(); - refresh(); - })); + entry.getAccount().refresh(Auth.getInstance().getMsApi()); } } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 8d2658761..530cc569b 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -94,11 +94,11 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Component.translatable("auth.notif.title"), Component.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(a -> { if (!a.isExpired()) { login(a); } - })).thenRun(this::save); + }).thenRun(this::save); } else { try { API.getInstance().shutdown(); @@ -131,14 +131,18 @@ protected void login(Account account) { } @Override - void showAccountsExpiredScreen(Account account) { + CompletableFuture showAccountsExpiredScreen(Account account) { Screen current = mc.screen; + var fut = new CompletableFuture(); mc.execute(() -> mc.setScreen(new ConfirmScreen((bl) -> { - mc.setScreen(current); if (bl) { - msApi.startDeviceAuth(); + msApi.startDeviceAuth().thenRun(() -> fut.complete(account)); + } else { + fut.cancel(true); } + mc.setScreen(current); }, Component.translatable("auth"), Component.translatable("auth.accountExpiredNotice", account.getName())))); + return fut; } @Override diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index fba1d8b3f..28e50dcb2 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -78,6 +78,7 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; + private boolean triedAccountRefresh; public SkinManagementScreen(Screen parent, Account account) { super(Component.translatable("skins.manage")); @@ -91,14 +92,20 @@ protected void init() { int headerHeight = 33; int contentHeight = height - headerHeight * 2; - addRenderableWidget(new StringWidget(0, headerHeight/2-font.lineHeight/2, width, font.lineHeight, getTitle(), getFont())); - var back = addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, btn -> onClose()) - .bounds(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); + StringWidget titleWidget = new StringWidget(0, headerHeight / 2 - font.lineHeight / 2, width, font.lineHeight, getTitle(), getFont()); + if (!triedAccountRefresh) { + addRenderableWidget(titleWidget); + } + var back = Button.builder(CommonComponents.GUI_BACK, btn -> onClose()) + .bounds(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build(); var loadingPlaceholder = new LoadingDotsWidget(getFont(), Component.translatable("skins.loading")); loadingPlaceholder.setRectangle(width, contentHeight, 0, headerHeight); - addRenderableWidget(loadingPlaceholder); + if (!triedAccountRefresh) { + addRenderableWidget(loadingPlaceholder); + addRenderableWidget(back); + } skinList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); @@ -137,8 +144,8 @@ protected void init() { skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { - removeWidget(back); - removeWidget(loadingPlaceholder); + clearWidgets(); + addRenderableWidget(titleWidget); addRenderableWidget(current); addRenderableWidget(skinsTab); addRenderableWidget(capesTab); @@ -153,7 +160,14 @@ protected void init() { } CompletableFuture fut; if (account.needsRefresh()) { - fut = account.refresh(Auth.getInstance().getMsApi()); + if (triedAccountRefresh) { + fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { + }); + } else { + triedAccountRefresh = true; + account.refresh(Auth.getInstance().getMsApi()); + return; + } } else { fut = CompletableFuture.completedFuture(null); } @@ -163,12 +177,16 @@ protected void init() { initDisplay(); addWidgets.run(); }).exceptionally(t -> { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (!triedAccountRefresh) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + } var error = Component.translatable("skins.error.failed_to_load"); - var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); - removeWidget(loadingPlaceholder); + var errorDesc = Component.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + clearWidgets(); + addRenderableWidget(titleWidget); addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); addRenderableWidget(new StringWidget(width / 2 - getFont().width(errorDesc) / 2, height / 2 + 1, getFont().width(errorDesc), getFont().lineHeight, errorDesc, getFont())); + addRenderableWidget(back); return null; }); } @@ -460,9 +478,9 @@ public Entry(int height, SkinWidget widget, @Nullable Component label) { } } btn.active = true; - }, Component.translatable("skins.manage.delete.confirm"), asset.active() ? + }, Component.translatable("skins.manage.delete.confirm"), (asset.active() ? Component.translatable("skins.manage.delete.confirm.desc_active") : - Component.translatable("skins.manage.delete.confirm.desc") + Component.translatable("skins.manage.delete.confirm.desc")) .withColor(Colors.RED.toInt()))); }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11) .build(); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 1b260737d..4faaef659 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -134,10 +134,7 @@ private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedOrNull(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> client.execute(() -> { - Auth.getInstance().save(); - refresh(); - })); + entry.getAccount().refresh(Auth.getInstance().getMsApi()); } } diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 13238701d..7922c7dd0 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -23,6 +23,7 @@ package io.github.axolotlclient.modules.auth; import java.util.*; +import java.util.concurrent.CompletableFuture; import com.mojang.authlib.minecraft.UserApiService; import com.mojang.authlib.yggdrasil.ProfileResult; @@ -93,11 +94,11 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus(Text.translatable("auth.notif.title"), Text.translatable("auth.notif.refreshing", account.getName())); } - account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(a -> { if (!a.isExpired()) { login(a); } - })).thenRun(this::save); + }).thenRun(this::save); } else { try { API.getInstance().shutdown(); @@ -126,14 +127,18 @@ protected void login(Account account) { } @Override - void showAccountsExpiredScreen(Account account) { + CompletableFuture showAccountsExpiredScreen(Account account) { Screen current = client.currentScreen; + var fut = new CompletableFuture(); client.execute(() -> client.setScreen(new ConfirmScreen((bl) -> { - client.setScreen(current); if (bl) { - msApi.startDeviceAuth(); + msApi.startDeviceAuth().thenRun(() -> fut.complete(account)); + } else { + fut.cancel(true); } + client.setScreen(current); }, Text.translatable("auth"), Text.translatable("auth.accountExpiredNotice", account.getName())))); + return fut; } @Override diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 9bf130cca..bd8d2cd9e 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -78,6 +78,7 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; + private boolean triedAccountRefresh; public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); @@ -91,14 +92,19 @@ protected void init() { int headerHeight = 33; int contentHeight = height - headerHeight * 2; - addDrawableSelectableElement(new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight/2, width, textRenderer.fontHeight, getTitle(), textRenderer)); - var back = addDrawableSelectableElement(ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) - .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); + var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight/2, width, textRenderer.fontHeight, getTitle(), textRenderer); + if (!triedAccountRefresh) { + addDrawableSelectableElement(titleWidget); + } + var back = ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) + .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build(); var loadingPlaceholder = new LoadingTextWidget(textRenderer, Text.translatable("skins.loading")); loadingPlaceholder.setDimensionsAndPosition(width, contentHeight, 0, headerHeight); - addDrawableSelectableElement(loadingPlaceholder); - addDrawableSelectableElement(back); + if (!triedAccountRefresh) { + addDrawableSelectableElement(loadingPlaceholder); + addDrawableSelectableElement(back); + } skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); @@ -137,8 +143,8 @@ protected void init() { skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { - remove(back); - remove(loadingPlaceholder); + clearChildren(); + addDrawableSelectableElement(titleWidget); addDrawableSelectableElement(current); addDrawableSelectableElement(skinsTab); addDrawableSelectableElement(capesTab); @@ -153,7 +159,14 @@ protected void init() { } CompletableFuture fut; if (account.needsRefresh()) { - fut = account.refresh(Auth.getInstance().getMsApi()); + if (triedAccountRefresh) { + fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { + }); + } else { + triedAccountRefresh = true; + account.refresh(Auth.getInstance().getMsApi()); + return; + } } else { fut = CompletableFuture.completedFuture(null); } @@ -163,12 +176,16 @@ protected void init() { initDisplay(); addWidgets.run(); }).exceptionally(t -> { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (!triedAccountRefresh) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + } var error = Text.translatable("skins.error.failed_to_load"); - var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); - remove(loadingPlaceholder); + var errorDesc = Text.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + clearChildren(); + addDrawableSelectableElement(titleWidget); addDrawableSelectableElement(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); addDrawableSelectableElement(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc, textRenderer)); + addDrawableSelectableElement(back); return null; }); } @@ -469,9 +486,9 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { } } btn.active = true; - }, Text.translatable("skins.manage.delete.confirm"), asset.active() ? + }, Text.translatable("skins.manage.delete.confirm"), (asset.active() ? Text.translatable("skins.manage.delete.confirm.desc_active") : - Text.translatable("skins.manage.delete.confirm.desc") + Text.translatable("skins.manage.delete.confirm.desc")) .setColor(Colors.RED.toInt()))); }, true).sprite(Identifier.of("axolotlclient", "delete"), 7, 7).dimensions(11, 11) .build(); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 6ebf5451e..8e31f88fa 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -212,10 +212,7 @@ private void refreshAccount() { refreshButton.active = false; AccountsListWidget.Entry entry = accountsListWidget.getSelectedEntry(); if (entry != null) { - entry.getAccount().refresh(Auth.getInstance().getMsApi()).thenRun(() -> minecraft.submit(() -> { - Auth.getInstance().save(); - refresh(); - })); + entry.getAccount().refresh(Auth.getInstance().getMsApi()); } } } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index 2624af180..af036c078 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -23,6 +23,7 @@ package io.github.axolotlclient.modules.auth; import java.util.*; +import java.util.concurrent.CompletableFuture; import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; @@ -87,11 +88,11 @@ protected void login(Account account) { if (account.isExpired()) { Notifications.getInstance().addStatus("auth.notif.title", "auth.notif.refreshing", account.getName()); } - account.refresh(msApi).thenAccept(res -> res.ifPresent(a -> { + account.refresh(msApi).thenAccept(a -> { if (!a.isExpired()) { login(a); } - })).thenRun(this::save); + }).thenRun(this::save); } else { try { API.getInstance().shutdown(); @@ -138,14 +139,18 @@ public void loadTextures(String uuid, String name) { } @Override - void showAccountsExpiredScreen(Account account) { + CompletableFuture showAccountsExpiredScreen(Account account) { Screen current = client.screen; + var fut = new CompletableFuture(); client.submit(() -> client.openScreen(new ConfirmScreen((bl, i) -> { - client.openScreen(current); if (bl) { - msApi.startDeviceAuth(); + msApi.startDeviceAuth().thenRun(() -> fut.complete(account)); + } else { + fut.cancel(true); } + client.openScreen(current); }, I18n.translate("auth"), I18n.translate("auth.accountExpiredNotice", account.getName()), 1))); + return fut; } @Override diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java index 31e070420..021c0f651 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java @@ -83,9 +83,9 @@ public static Account deserialize(JsonObject object) { return new Account(uuid, name, authToken, msaToken, refreshToken, expiration); } - public CompletableFuture> refresh(MSApi auth) { - if (isOffline()) return CompletableFuture.completedFuture(Optional.empty()); - return auth.refreshToken(refreshToken, this); + public CompletableFuture refresh(MSApi auth) { + if (isOffline()) return CompletableFuture.failedFuture(new UnsupportedOperationException()); + return auth.refresh(this); } public boolean isOffline() { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java index a1adec819..2ff257eab 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Accounts.java @@ -27,6 +27,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -120,7 +121,7 @@ public boolean allowOfflineAccounts() { return !accounts.isEmpty() && !accounts.stream().allMatch(Account::isOffline); } - abstract void showAccountsExpiredScreen(Account account); + abstract CompletableFuture showAccountsExpiredScreen(Account account); abstract void displayDeviceCode(DeviceFlowData data); } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index 61f3b6b86..c0c191c00 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -31,8 +31,8 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Locale; -import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import com.github.mizosoft.methanol.FormBodyPublisher; @@ -75,7 +75,7 @@ public MSApi(Accounts accounts, Supplier languageSupplier) { INSTANCE = this; } - public CompletableFuture startDeviceAuth() { + public CompletableFuture startDeviceAuth() { String[] lang = languageSupplier.get().replace("_", "-").split("-"); logger.debug("starting ms device auth flow"); @@ -87,7 +87,7 @@ public CompletableFuture startDeviceAuth() { .header("content-type", "application/x-www-form-urlencoded") .uri(URI.create(MS_DEVICE_CODE_LOGIN_URL + lang[0] + "-" + lang[1].toUpperCase(Locale.ROOT))); return requestJson(builder.build()) - .thenApply(object -> { + .thenApplyAsync(object -> { int expiresIn = object.get("expires_in").getAsInt(); String deviceCode = object.get("device_code").getAsString(); String userCode = object.get("user_code").getAsString(); @@ -98,8 +98,7 @@ public CompletableFuture startDeviceAuth() { DeviceFlowData data = new DeviceFlowData(message, verificationUri, deviceCode, userCode, expiresIn, interval); accounts.displayDeviceCode(data); return data; - }) - .thenApply(data -> { + }).thenComposeAsync(data -> { logger.debug("waiting for user authorization..."); long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < data.getExpiresIn() * 1000L && !data.isCancelled()) { @@ -117,19 +116,26 @@ public CompletableFuture startDeviceAuth() { data.setStatus("auth.working"); return authenticateFromMSTokens(response.get("access_token").getAsString(), response.get("refresh_token").getAsString()) - .thenAccept(o -> { - o.ifPresent(a -> { - int index = accounts.getAccounts().indexOf(a); - if (index == -1) { - accounts.getAccounts().add(a); - } else { - accounts.getAccounts().set(index, a); - } - accounts.login(a); - accounts.save(); - data.setStatus("auth.finished"); - }); - }).join(); + .thenApply(a -> { + int index = accounts.getAccounts().indexOf(a); + Account loginAccount; + if (index == -1) { + accounts.getAccounts().add(a); + loginAccount = a; + } else { + var prev = accounts.getAccounts().get(index); + prev.setAuthToken(a.getAuthToken()); + prev.setExpiration(a.getExpiration()); + prev.setMsaToken(a.getMsaToken()); + prev.setName(a.getName()); + prev.setRefreshToken(a.getRefreshToken()); + loginAccount = prev; + } + accounts.login(loginAccount); + accounts.save(); + data.setStatus("auth.finished"); + return loginAccount; + }); } if (response.has("error")) { @@ -147,11 +153,11 @@ public CompletableFuture startDeviceAuth() { } } } - return null; + return CompletableFuture.failedStage(new TimeoutException()); }); } - private CompletableFuture> authenticateFromMSTokens(String accessToken, String refreshToken) { + private CompletableFuture authenticateFromMSTokens(String accessToken, String refreshToken) { return CompletableFuture.supplyAsync(() -> { logger.debug("getting xbl token... "); XblData xbl = authXbl(accessToken).join(); @@ -163,17 +169,17 @@ private CompletableFuture> authenticateFromMSTokens(String acc JsonObject profileJson = getMCProfile(mc.accessToken()).join(); if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.login.failed", "auth.notif.login.failed.no_profile"); - return Optional.empty(); + throw new IllegalStateException(); } logger.debug("retrieving entitlements..."); if (!checkOwnership(mc.accessToken()).join()) { AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.login.failed", "auth.notif.login.failed.no_entitlement"); logger.warn("Failed to check for game ownership!"); - return Optional.empty(); + throw new IllegalStateException(); } logger.debug("getting profile..."); MCProfile profile = MCProfile.get(profileJson); - return Optional.of(new Account(profile.name(), profile.id(), mc.accessToken(), mc.expiration(), refreshToken, accessToken)); + return new Account(profile.name(), profile.id(), mc.accessToken(), mc.expiration(), refreshToken, accessToken); }); } @@ -187,7 +193,8 @@ public static MCProfile get(JsonObject json) { .toList()); } - public record OnlineSkin(String id, String state, String url, String variant, String textureKey) implements Skin { + public record OnlineSkin(String id, String state, String url, String variant, + String textureKey) implements Skin { public static final String VARIANT_CLASSIC = "CLASSIC"; public static final String VARIANT_SLIM = "SLIM"; public static final String STATE_ACTIVE = "ACTIVE"; @@ -198,7 +205,7 @@ public static OnlineSkin get(JsonObject object) { object.get("state").getAsString(), url, object.get("variant").getAsString(), - url.substring(url.lastIndexOf("/")+1)); + url.substring(url.lastIndexOf("/") + 1)); } @Override @@ -245,7 +252,7 @@ public record OnlineCape(String id, String state, String url, String alias, Stri public static OnlineCape get(JsonObject object) { String url = object.get("url").getAsString(); return new OnlineCape(object.get("id").getAsString(), object.get("state").getAsString(), - url, object.get("alias").getAsString(), url.substring(url.lastIndexOf("/")+1)); + url, object.get("alias").getAsString(), url.substring(url.lastIndexOf("/") + 1)); } public CompletableFuture image() { @@ -340,44 +347,39 @@ private CompletableFuture getMCProfile(String accessToken) { .header("Authorization", "Bearer " + accessToken).build()); } - public CompletableFuture> refreshToken(String token, Account account) { - return CompletableFuture.supplyAsync(() -> { - logger.debug("refreshing auth code... "); - HttpRequest.Builder requestBuilder = HttpRequest - .newBuilder(URI.create(MS_TOKEN_LOGIN_URL)) - .POST(FormBodyPublisher.newBuilder() - .query("client_id", CLIENT_ID) - .query("refresh_token", token) - .query("scope", SCOPES) - .query("grant_type", "refresh_token").build()) - .header("Accept", "application/json"); - - JsonObject response = requestJson(requestBuilder.build()).join(); - - if (response.has("error_codes")) { - int errorCode = response.get("error_codes").getAsJsonArray().get(0).getAsInt(); - if (errorCode == 70000 || errorCode == 70012) { - accounts.showAccountsExpiredScreen(account); - } else { - logger.warn("Login error, unexpected response: " + response); - AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.refresh.error", "auth.notif.refresh.error.unexpected_response"); + public CompletableFuture refresh(Account account) { + logger.debug("refreshing auth code..."); + return requestJson(HttpRequest + .newBuilder(URI.create(MS_TOKEN_LOGIN_URL)) + .POST(FormBodyPublisher.newBuilder() + .query("client_id", CLIENT_ID) + .query("refresh_token", account.getRefreshToken()) + .query("scope", SCOPES) + .query("grant_type", "refresh_token").build()) + .header("Accept", "application/json").build()) + .thenCompose(response -> { + if (response.has("error_codes")) { + int errorCode = response.get("error_codes").getAsJsonArray().get(0).getAsInt(); + if (errorCode == 70000 || errorCode == 70012) { + return accounts.showAccountsExpiredScreen(account); + } else { + logger.warn("Login error, unexpected response: " + response); + AxolotlClientCommon.getInstance().getNotificationProvider().addStatus("auth.notif.refresh.error", "auth.notif.refresh.error.unexpected_response"); + throw new IllegalArgumentException(); + } } - return Optional.empty(); - } - - logger.debug("authenticating..."); - Optional opt = authenticateFromMSTokens(response.get("access_token").getAsString(), - response.get("refresh_token").getAsString()).join(); - opt.ifPresent(refreshed -> { - account.setRefreshToken(refreshed.getRefreshToken()); - account.setAuthToken(refreshed.getAuthToken()); - account.setName(refreshed.getName()); - account.setMsaToken(refreshed.getMsaToken()); - account.setExpiration(refreshed.getExpiration()); - accounts.save(); + logger.debug("authenticating..."); + return authenticateFromMSTokens(response.get("access_token").getAsString(), + response.get("refresh_token").getAsString()).thenApply(refreshed -> { + account.setRefreshToken(refreshed.getRefreshToken()); + account.setAuthToken(refreshed.getAuthToken()); + account.setName(refreshed.getName()); + account.setMsaToken(refreshed.getMsaToken()); + account.setExpiration(refreshed.getExpiration()); + accounts.save(); + return account; + }); }); - return opt; - }); } private CompletableFuture requestJson(HttpRequest request) { diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index a4b65551c..2c2552722 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -817,6 +817,7 @@ "skins.nav.capes": "Capes", "skins.error.failed_to_load": "Failed to load skins", "skins.error.failed_to_load_desc": "Your log file may include more information.", + "skins.error.failed_to_load_not_refreshed": "Could not refresh the selected account.", "skins.capes.no_cape": "No Cape", "skins.manage.equipped": "Equipped", "skins.manage.equip": "Equip", From 1fa06a75554e2e8624df6eec0cf8f176a948c949 Mon Sep 17 00:00:00 2001 From: moehreag Date: Fri, 5 Sep 2025 13:51:33 +0200 Subject: [PATCH 09/23] fix account refreshing --- .../auth/skin/SkinManagementScreen.java | 38 +++++++--------- .../modules/auth/skin/SkinRenderer.java | 22 ++++++--- .../auth/skin/SkinManagementScreen.java | 43 ++++++++---------- .../auth/skin/SkinManagementScreen.java | 45 +++++++++---------- .../auth/skin/SkinManagementScreen.java | 45 ++++++++----------- .../assets/axolotlclient/lang/en_us.json | 1 - 6 files changed, 90 insertions(+), 104 deletions(-) diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 423e9701c..6ddcf5d63 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -78,13 +79,18 @@ public class SkinManagementScreen extends Screen { private SkinWidget current; private final Watcher skinDirWatcher; private final List drawables = new ArrayList<>(); - private boolean triedAccountRefresh; + private final CompletableFuture refreshFuture; public SkinManagementScreen(Screen parent, Account account) { super(new TranslatableText("skins.manage")); this.parent = parent; this.account = account; skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + if (account.needsRefresh()) { + refreshFuture = account.refresh(Auth.getInstance().getMsApi()); + } else { + refreshFuture = CompletableFuture.completedFuture(null); + } } @Override @@ -115,10 +121,9 @@ protected MutableText getNarrationMessage() { } }; loadingPlaceholder.active = false; - if (!triedAccountRefresh) { - addDrawableChild(loadingPlaceholder); - addDrawableChild(back); - } + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setLeftPos(width / 2); @@ -170,30 +175,18 @@ protected MutableText getNarrationMessage() { addWidgets.run(); return; } - CompletableFuture fut; - if (account.needsRefresh()) { - if (triedAccountRefresh) { - fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { - }); - } else { - triedAccountRefresh = true; - account.refresh(Auth.getInstance().getMsApi()); - return; - } - } else { - fut = CompletableFuture.completedFuture(null); - } - fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + refreshFuture.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) .thenAcceptAsync(profile -> { cachedProfile = profile; initDisplay(); addWidgets.run(); }).exceptionally(t -> { - if (!triedAccountRefresh) { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (t.getCause() instanceof CancellationException) { + client.openScreen(parent); + return null; } var error = new TranslatableText("skins.error.failed_to_load"); - var errorDesc = new TranslatableText(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + var errorDesc = new TranslatableText("skins.error.failed_to_load_desc"); clear(); addDrawableChild(back); class TextWidget extends AbstractButtonWidget { @@ -611,6 +604,7 @@ public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float refreshCurrentList(); }).exceptionally(t -> { AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + equipping = false; return null; }); }); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index c4be27ad7..560d78b02 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -22,6 +22,8 @@ package io.github.axolotlclient.modules.auth.skin; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.*; import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.client.util.math.MatrixStack; @@ -69,19 +71,29 @@ public static void render(MatrixStack graphics, boolean classicVariant, graphics.scale(1.0F, 1.0F, -1.0F); graphics.translate(0.0F, -1.5F, 0.0F); var model = classicVariant ? classicModel : slimModel; - RenderLayer renderLayer = model.getLayer(skinTexture); var tessellator = Tessellator.getInstance(); - var buf = VertexConsumerProvider.immediate(tessellator.getBuffer()); - model.render(graphics, buf.getBuffer(renderLayer), 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); + RenderSystem.enableDepthTest(); + RenderSystem.enableCull(); + RenderSystem.enableBlend(); if (cape != null) { + graphics.push(); + MinecraftClient.getInstance().getTextureManager().bindTexture(cape); graphics.translate(0.0F, 0.0F, 0.125F); graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F)); graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F)); - model.renderCape(graphics, buf.getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); + model.renderCape(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); + tessellator.draw(); + graphics.pop(); } - graphics.pop(); + MinecraftClient.getInstance().getTextureManager().bindTexture(skinTexture); + model.render(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(model.getLayer(skinTexture)), 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); tessellator.draw(); graphics.pop(); + + graphics.pop(); + RenderSystem.disableBlend(); + RenderSystem.disableDepthTest(); + RenderSystem.disableCull(); DiffuseLighting.enableGuiDepthLighting(); } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 40ae65299..ef845548a 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.function.Supplier; @@ -71,13 +72,18 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; - private boolean triedAccountRefresh; + private final CompletableFuture refreshFuture; public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + if (account.needsRefresh()) { + refreshFuture = account.refresh(Auth.getInstance().getMsApi()); + } else { + refreshFuture = CompletableFuture.completedFuture(null); + } } @Override @@ -86,9 +92,8 @@ protected void init() { int contentHeight = height - headerHeight * 2; var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle(), textRenderer); - if (!triedAccountRefresh) { - addDrawableChild(titleWidget); - } + addDrawableChild(titleWidget); + var back = addDrawableChild(ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build()); @@ -109,10 +114,9 @@ protected void updateNarration(NarrationMessageBuilder builder) { } }; loadingPlaceholder.active = false; - if (!triedAccountRefresh) { - addDrawableChild(loadingPlaceholder); - addDrawableChild(back); - } + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setLeftPos(width / 2); @@ -165,30 +169,18 @@ protected void updateNarration(NarrationMessageBuilder builder) { addWidgets.run(); return; } - CompletableFuture fut; - if (account.needsRefresh()) { - if (triedAccountRefresh) { - fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { - }); - } else { - triedAccountRefresh = true; - account.refresh(Auth.getInstance().getMsApi()); - return; - } - } else { - fut = CompletableFuture.completedFuture(null); - } - fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + refreshFuture.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) .thenAcceptAsync(profile -> { cachedProfile = profile; initDisplay(); addWidgets.run(); }).exceptionally(t -> { - if (!triedAccountRefresh) { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (t.getCause() instanceof CancellationException) { + client.setScreen(parent); + return null; } var error = Text.translatable("skins.error.failed_to_load"); - var errorDesc = Text.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); clearChildren(); addDrawableChild(titleWidget); addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); @@ -588,6 +580,7 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float refreshCurrentList(); }).exceptionally(t -> { AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + equipping = false; return null; }); }).width(widget.getWidth()).build(); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 28e50dcb2..e6648b1d2 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -78,13 +79,18 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; - private boolean triedAccountRefresh; + private final CompletableFuture refreshFuture; public SkinManagementScreen(Screen parent, Account account) { super(Component.translatable("skins.manage")); this.parent = parent; this.account = account; skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + if (account.needsRefresh()) { + refreshFuture = account.refresh(Auth.getInstance().getMsApi()); + } else { + refreshFuture = CompletableFuture.completedFuture(null); + } } @Override @@ -93,19 +99,17 @@ protected void init() { int contentHeight = height - headerHeight * 2; StringWidget titleWidget = new StringWidget(0, headerHeight / 2 - font.lineHeight / 2, width, font.lineHeight, getTitle(), getFont()); - if (!triedAccountRefresh) { - addRenderableWidget(titleWidget); - } + addRenderableWidget(titleWidget); + var back = Button.builder(CommonComponents.GUI_BACK, btn -> onClose()) .bounds(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build(); var loadingPlaceholder = new LoadingDotsWidget(getFont(), Component.translatable("skins.loading")); loadingPlaceholder.setRectangle(width, contentHeight, 0, headerHeight); - if (!triedAccountRefresh) { - addRenderableWidget(loadingPlaceholder); - addRenderableWidget(back); - } + addRenderableWidget(loadingPlaceholder); + addRenderableWidget(back); + skinList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); @@ -158,30 +162,20 @@ protected void init() { addWidgets.run(); return; } - CompletableFuture fut; - if (account.needsRefresh()) { - if (triedAccountRefresh) { - fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { - }); - } else { - triedAccountRefresh = true; - account.refresh(Auth.getInstance().getMsApi()); - return; - } - } else { - fut = CompletableFuture.completedFuture(null); - } - fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + + refreshFuture.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) .thenAcceptAsync(profile -> { cachedProfile = profile; initDisplay(); addWidgets.run(); }).exceptionally(t -> { - if (!triedAccountRefresh) { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (t.getCause() instanceof CancellationException) { + minecraft.setScreen(parent); + return null; } + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = Component.translatable("skins.error.failed_to_load"); - var errorDesc = Component.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + var errorDesc = Component.translatable("skins.error.failed_to_load_desc"); clearWidgets(); addRenderableWidget(titleWidget); addRenderableWidget(new StringWidget(width / 2 - getFont().width(error) / 2, height / 2 - getFont().lineHeight - 2, getFont().width(error), getFont().lineHeight, error, getFont())); @@ -529,6 +523,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo refreshCurrentList(); }).exceptionally(t -> { AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + equipping = false; return null; }); }).width(widget.getWidth()).build(); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index bd8d2cd9e..f75ad731e 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -78,13 +79,18 @@ public class SkinManagementScreen extends Screen { private boolean capesTab; private SkinWidget current; private final Watcher skinDirWatcher; - private boolean triedAccountRefresh; + private final CompletableFuture refreshFuture; public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + if (account.needsRefresh()) { + refreshFuture = account.refresh(Auth.getInstance().getMsApi()); + } else { + refreshFuture = CompletableFuture.completedFuture(null); + } } @Override @@ -92,19 +98,17 @@ protected void init() { int headerHeight = 33; int contentHeight = height - headerHeight * 2; - var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight/2, width, textRenderer.fontHeight, getTitle(), textRenderer); - if (!triedAccountRefresh) { - addDrawableSelectableElement(titleWidget); - } + var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle(), textRenderer); + addDrawableSelectableElement(titleWidget); + var back = ButtonWidget.builder(CommonTexts.BACK, btn -> closeScreen()) .positionAndSize(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20).build(); var loadingPlaceholder = new LoadingTextWidget(textRenderer, Text.translatable("skins.loading")); loadingPlaceholder.setDimensionsAndPosition(width, contentHeight, 0, headerHeight); - if (!triedAccountRefresh) { - addDrawableSelectableElement(loadingPlaceholder); - addDrawableSelectableElement(back); - } + addDrawableSelectableElement(loadingPlaceholder); + addDrawableSelectableElement(back); + skinList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); capesList = new SkinListWidget(client, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); skinList.setX(width / 2); @@ -157,30 +161,18 @@ protected void init() { addWidgets.run(); return; } - CompletableFuture fut; - if (account.needsRefresh()) { - if (triedAccountRefresh) { - fut = CompletableFuture.failedFuture(new Throwable(null, null, false, false) { - }); - } else { - triedAccountRefresh = true; - account.refresh(Auth.getInstance().getMsApi()); - return; - } - } else { - fut = CompletableFuture.completedFuture(null); - } - fut.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + refreshFuture.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) .thenAcceptAsync(profile -> { cachedProfile = profile; initDisplay(); addWidgets.run(); }).exceptionally(t -> { - if (!triedAccountRefresh) { - AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + if (t.getCause() instanceof CancellationException) { + client.setScreen(parent); + return null; } var error = Text.translatable("skins.error.failed_to_load"); - var errorDesc = Text.translatable(triedAccountRefresh ? "skins.error.failed_to_load_not_refreshed" : "skins.error.failed_to_load_desc"); + var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); clearChildren(); addDrawableSelectableElement(titleWidget); addDrawableSelectableElement(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); @@ -537,6 +529,7 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float refreshCurrentList(); }).exceptionally(t -> { AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + equipping = false; return null; }); }).width(widget.getWidth()).build(); diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index 2c2552722..a4b65551c 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -817,7 +817,6 @@ "skins.nav.capes": "Capes", "skins.error.failed_to_load": "Failed to load skins", "skins.error.failed_to_load_desc": "Your log file may include more information.", - "skins.error.failed_to_load_not_refreshed": "Could not refresh the selected account.", "skins.capes.no_cape": "No Cape", "skins.manage.equipped": "Equipped", "skins.manage.equip": "Equip", From 6c018099b53538ed0d7c678777b60acc7396c221 Mon Sep 17 00:00:00 2001 From: moehreag Date: Fri, 5 Sep 2025 17:02:58 +0200 Subject: [PATCH 10/23] fix json construction - no records :/ --- .../modules/auth/skin/SkinRenderer.java | 2 - .../axolotlclient/modules/auth/MSApi.java | 51 ++++---- .../axolotlclient/util/JsonBuilders.java | 110 ++++++++++++++++++ 3 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 560d78b02..433d0146c 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -73,7 +73,6 @@ public static void render(MatrixStack graphics, boolean classicVariant, var model = classicVariant ? classicModel : slimModel; var tessellator = Tessellator.getInstance(); RenderSystem.enableDepthTest(); - RenderSystem.enableCull(); RenderSystem.enableBlend(); if (cape != null) { graphics.push(); @@ -93,7 +92,6 @@ public static void render(MatrixStack graphics, boolean classicVariant, graphics.pop(); RenderSystem.disableBlend(); RenderSystem.disableDepthTest(); - RenderSystem.disableCull(); DiffuseLighting.enableGuiDepthLighting(); } } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index c0c191c00..b19c756e5 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -38,13 +38,12 @@ import com.github.mizosoft.methanol.FormBodyPublisher; import com.github.mizosoft.methanol.MediaType; import com.github.mizosoft.methanol.MultipartBodyPublisher; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.modules.auth.skin.Cape; import io.github.axolotlclient.modules.auth.skin.Skin; import io.github.axolotlclient.util.GsonHelper; +import io.github.axolotlclient.util.JsonBuilders; import io.github.axolotlclient.util.Logger; import io.github.axolotlclient.util.NetworkUtil; @@ -63,7 +62,6 @@ public class MSApi { private final Logger logger; private final Accounts accounts; private final HttpClient client; - private final Gson gson = new GsonBuilder().create(); public static MSApi INSTANCE; @@ -282,17 +280,15 @@ public CompletableFuture equip(MSApi api, Account account) { } private CompletableFuture authXbl(String code) { - JsonObject object = new JsonObject(); - JsonObject properties = new JsonObject(); - properties.addProperty("AuthMethod", "RPS"); - properties.addProperty("SiteName", "user.auth.xboxlive.com"); - properties.addProperty("RpsTicket", "d=" + code); - object.add("Properties", properties); - object.addProperty("RelyingParty", "http://auth.xboxlive.com"); - object.addProperty("TokenType", "JWT"); HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(XBL_AUTH_URL)) - .POST(HttpRequest.BodyPublishers.ofString(object.toString())) + .POST(HttpRequest.BodyPublishers.ofString(JsonBuilders.JsonObject.create() + .field("Properties", JsonBuilders.JsonObject.create() + .field("AuthMethod", "RPS") + .field("SiteName", "user.auth.xboxlive.com") + .field("RpsTicket", "d=" + code)) + .field("RelyingParty", "http://auth.xboxlive.com") + .field("TokenType", "JWT").asString())) .header("content-type", "application/json") .header("accept", "application/json"); @@ -306,24 +302,20 @@ private record DisplayClaims(String uhs) { } private CompletableFuture authXstsMC(String xblToken) { - String body = "{" + - " \"Properties\": {" + - " \"SandboxId\": \"RETAIL\"," + - " \"UserTokens\": [" + - " \"" + xblToken + "\"" + - " ]" + - " }," + - " \"RelyingParty\": \"rp://api.minecraftservices.com/\"," + - " \"TokenType\": \"JWT\"" + - " }"; - return requestJson(HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(body)).uri(URI.create(XBL_XSTS_AUTH_URL)).build()) + var body = JsonBuilders.JsonObject.create() + .field("Properties", JsonBuilders.JsonObject.create() + .field("SandboxId", "RETAIL")) + .field("UserTokens", JsonBuilders.JsonArray.create().field(xblToken)) + .field("RelyingParty", "rp://api.minecraftservices.com/") + .field("TokenType", "JWT"); + return requestJson(HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(body.asString())).uri(URI.create(XBL_XSTS_AUTH_URL)).build()) .thenApply(response -> new XblData(Instant.parse(response.get("IssueInstant").getAsString()), Instant.parse(response.get("NotAfter").getAsString()), response.get("Token").getAsString(), new XblData.DisplayClaims(response.get("DisplayClaims").getAsJsonObject().get("xui").getAsJsonArray().get(0).getAsJsonObject().get("uhs").getAsString()))); } private CompletableFuture authMC(String userhash, String xsts) { - String body = "{\"identityToken\": \"XBL3.0 x=" + userhash + ";" + xsts + "\"\n}"; - return requestJson(HttpRequest.newBuilder(URI.create(MC_LOGIN_WITH_XBOX_URL)).POST(HttpRequest.BodyPublishers.ofString(body)).build()) + var body = JsonBuilders.JsonObject.create().field("identityToken", "XBL3.0 x=" + userhash + ";" + xsts); + return requestJson(HttpRequest.newBuilder(URI.create(MC_LOGIN_WITH_XBOX_URL)).POST(HttpRequest.BodyPublishers.ofString(body.asString())).build()) .thenApply(response -> new MCXblData(response.get("username").getAsString(), response.get("access_token").getAsString(), Instant.now().plus(response.get("expires_in").getAsLong(), ChronoUnit.SECONDS))); @@ -403,12 +395,11 @@ private MCProfile extractProfile(JsonObject profileJson) { } public CompletableFuture setSkin(Account account, MCProfile.OnlineSkin skin) { - record Body(String variant, String url) { - } return requestJson(HttpRequest.newBuilder() .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) .header("Authorization", "Bearer " + account.getAuthToken()) - .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(new Body(skin.variant(), skin.url())))).build()) + .POST(HttpRequest.BodyPublishers.ofString(JsonBuilders.JsonObject.create() + .field("variant", skin.variant()).field("url", skin.url()).asString())).build()) .thenApply(this::extractProfile); } @@ -443,12 +434,10 @@ public CompletableFuture hideCape(Account account) { } public CompletableFuture showCape(Account account, MCProfile.OnlineCape cape) { - record Body(String capeId) { - } return requestJson(HttpRequest.newBuilder() .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/capes/active")) .header("Authorization", "Bearer " + account.getAuthToken()) - .PUT(HttpRequest.BodyPublishers.ofString(gson.toJson(new Body(cape.id())))).build()) + .PUT(HttpRequest.BodyPublishers.ofString(JsonBuilders.JsonObject.create().field("capeId", cape.id()).asString())).build()) .thenApply(this::extractProfile); } } diff --git a/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java b/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java new file mode 100644 index 000000000..c865353e7 --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java @@ -0,0 +1,110 @@ +package io.github.axolotlclient.util; + +import com.google.gson.JsonElement; + +public final class JsonBuilders { + public static final class JsonArray { + private final com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + + public static JsonArray create() { + return new JsonArray(); + } + + private JsonArray() { + } + + public JsonArray field(JsonElement ele) { + arr.add(ele); + return this; + } + + public JsonArray field(JsonArray builder) { + return field(builder.build()); + } + + public JsonArray field(JsonObject builder) { + return field(builder.build()); + } + + public JsonArray field(Number prop) { + arr.add(prop); + return this; + } + + public JsonArray field(String prop) { + arr.add(prop); + return this; + } + + public JsonArray field(Boolean prop) { + arr.add(prop); + return this; + } + + public JsonArray field(Character prop) { + arr.add(prop); + return this; + } + + public com.google.gson.JsonArray build() { + return arr; + } + + public String asString() { + return arr.toString(); + } + } + + public static final class JsonObject { + + private final com.google.gson.JsonObject obj = new com.google.gson.JsonObject(); + + public static JsonObject create() { + return new JsonObject(); + } + + private JsonObject() { + } + + public JsonObject field(String name, JsonElement ele) { + obj.add(name, ele); + return this; + } + + public JsonObject field(String name, JsonObject builder) { + return field(name, builder.build()); + } + + public JsonObject field(String name, JsonArray builder) { + return field(name, builder.build()); + } + + public JsonObject field(String name, Number prop) { + obj.addProperty(name, prop); + return this; + } + + public JsonObject field(String name, String prop) { + obj.addProperty(name, prop); + return this; + } + + public JsonObject field(String name, Boolean prop) { + obj.addProperty(name, prop); + return this; + } + + public JsonObject field(String name, Character prop) { + obj.addProperty(name, prop); + return this; + } + + public com.google.gson.JsonObject build() { + return obj; + } + + public String asString() { + return obj.toString(); + } + } +} From 358f06333ff4546f4f61d45fa70d6ca2a6f92ae5 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 6 Sep 2025 00:41:20 +0200 Subject: [PATCH 11/23] 1.8.9 port --- .../auth/skin/SkinManagementScreen.java | 22 +- .../modules/auth/skin/SkinManager.java | 2 + .../auth/skin/SkinManagementScreen.java | 1 + .../modules/auth/skin/SkinManager.java | 2 +- .../auth/skin/SkinManagementScreen.java | 1 + .../modules/auth/AccountsScreen.java | 25 +- .../axolotlclient/modules/auth/Auth.java | 6 +- .../auth/skin/SkinManagementScreen.java | 809 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 144 ++++ .../modules/auth/skin/SkinRenderer.java | 108 +++ .../modules/auth/skin/SkinWidget.java | 130 +++ .../io/github/axolotlclient/util/Util.java | 10 +- .../axolotlclient/modules/auth/Account.java | 1 - .../axolotlclient/modules/auth/MSApi.java | 10 +- .../axolotlclient/util/JsonBuilders.java | 22 + gradle.properties | 2 +- 16 files changed, 1253 insertions(+), 42 deletions(-) create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 6ddcf5d63..fdc8a476d 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -185,6 +185,7 @@ protected MutableText getNarrationMessage() { client.openScreen(parent); return null; } + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = new TranslatableText("skins.error.failed_to_load"); var errorDesc = new TranslatableText("skins.error.failed_to_load_desc"); clear(); @@ -193,11 +194,12 @@ class TextWidget extends AbstractButtonWidget { public TextWidget(int x, int y, int width, int height, Text message, TextRenderer textRenderer) { super(x, y, width, height, message); + active = false; } @Override public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { - textRenderer.draw(matrices, getMessage(), x, y, -1); + drawCenteredText(matrices, textRenderer, getMessage(), x+getWidth()/2, y+getHeight()/2-textRenderer.fontHeight/2, -1); } } addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); @@ -630,14 +632,6 @@ public Element getFocused() { @Override public void setFocused(@Nullable Element child) { - /*if (this.focused != null) { - this.focused.setFocused(false); - } - - if (child != null) { - child.setFocused(true); - }*/ - this.focused = child; } @@ -728,16 +722,6 @@ public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float } } - /*@Override - protected void updateNarration(NarrationMessageBuilder narrationElementOutput) { - skinWidget.appendNarrations(narrationElementOutput); - actionButtons.forEach(w -> w.appendNarrations(narrationElementOutput)); - if (label != null) { - label.appendNarrations(narrationElementOutput); - } - equipButton.appendNarrations(narrationElementOutput); - }*/ - private static class GradientHoleRectangleRenderState { public static void render(MatrixStack graphics, int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index f862a1bbb..893c3be9c 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -50,6 +50,7 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + @SuppressWarnings("UnstableApiUsage") public Skin read(Path p) { boolean slim; String sha256; @@ -125,6 +126,7 @@ public void releaseAll() { loadedTextures.clear(); } + @SuppressWarnings("UnstableApiUsage") public String getDefaultSkinHash(Account account) { var skin = DefaultSkinHelper.getTexture(UUIDHelper.fromUndashed(account.getUuid())); var mc = MinecraftClient.getInstance(); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index ef845548a..eec67caf3 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -179,6 +179,7 @@ protected void updateNarration(NarrationMessageBuilder builder) { client.setScreen(parent); return null; } + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = Text.translatable("skins.error.failed_to_load"); var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); clearChildren(); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index eb7677faf..4ee94d8eb 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -55,7 +55,7 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { - slim = (ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0); + slim = ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0; } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); } catch (Exception e) { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index f75ad731e..ce8a46881 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -171,6 +171,7 @@ protected void init() { client.setScreen(parent); return null; } + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); var error = Text.translatable("skins.error.failed_to_load"); var errorDesc = Text.translatable("skins.error.failed_to_load_desc"); clearChildren(); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java index 8e31f88fa..a3d64b909 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/AccountsScreen.java @@ -22,6 +22,7 @@ package io.github.axolotlclient.modules.auth; +import io.github.axolotlclient.modules.auth.skin.SkinManagementScreen; import net.minecraft.client.gui.screen.ConfirmScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; @@ -35,6 +36,7 @@ public class AccountsScreen extends Screen { private ButtonWidget loginButton; private ButtonWidget deleteButton; private ButtonWidget refreshButton; + private ButtonWidget skinsButton; public AccountsScreen(Screen currentScreen) { title = I18n.translate("accounts"); @@ -112,13 +114,17 @@ protected void buttonClicked(ButtonWidget buttonWidget) { login(); break; case 2: + minecraft.openScreen(new SkinManagementScreen( + this, accountsListWidget.getSelectedEntry().getAccount())); + break; + case 3: if (Auth.getInstance().allowOfflineAccounts()) { minecraft.openScreen(new ConfirmScreen(this, I18n.translate("auth.add.choose"), "", I18n.translate("auth.add.offline"), I18n.translate("auth.add.ms"), 234)); } else { initMSAuth(); } break; - case 3: + case 4: AccountsListWidget.Entry entry = this.accountsListWidget.getSelectedEntry(); if (entry != null) { buttonWidget.active = false; @@ -126,7 +132,7 @@ protected void buttonClicked(ButtonWidget buttonWidget) { refresh(); } break; - case 4: + case 5: refreshAccount(); break; } @@ -139,14 +145,16 @@ public void init() { accountsListWidget.setAccounts(Auth.getInstance().getAccounts()); - buttons.add(loginButton = new ButtonWidget(1, this.width / 2 - 154, this.height - 52, 150, 20, I18n.translate("auth.login"))); + buttons.add(loginButton = new ButtonWidget(1, this.width / 2 - 154, this.height - 52, 100, 20, I18n.translate("auth.login"))); + + buttons.add(skinsButton = new ButtonWidget(2, this.width / 2 - 50, this.height - 52, 100, 20, I18n.translate("skins.manage"))); - this.buttons.add(new ButtonWidget(2, this.width / 2 + 4, this.height - 52, 150, 20, I18n.translate("auth.add"))); + this.buttons.add(new ButtonWidget(3, this.width / 2 + 4 + 50, this.height - 52, 100, 20, I18n.translate("auth.add"))); - this.buttons.add(this.deleteButton = new ButtonWidget(3, this.width / 2 - 50, this.height - 28, 100, 20, I18n.translate("selectServer.delete"))); + this.buttons.add(this.deleteButton = new ButtonWidget(4, this.width / 2 - 50, this.height - 28, 100, 20, I18n.translate("selectServer.delete"))); - this.buttons.add(refreshButton = new ButtonWidget(4, this.width / 2 - 154, this.height - 28, 100, 20, + this.buttons.add(refreshButton = new ButtonWidget(5, this.width / 2 - 154, this.height - 28, 100, 20, I18n.translate("auth.refresh"))); this.buttons.add(new ButtonWidget(0, this.width / 2 + 4 + 50, this.height - 28, 100, 20, @@ -190,9 +198,10 @@ private void updateButtonActivationStates() { AccountsListWidget.Entry entry = accountsListWidget.getSelectedEntry(); if (minecraft.world == null && entry != null) { loginButton.active = entry.getAccount().isExpired() || !entry.getAccount().equals(Auth.getInstance().getCurrent()); - deleteButton.active = refreshButton.active = true; + refreshButton.active = skinsButton.active = !entry.getAccount().isOffline(); + deleteButton.active = true; } else { - loginButton.active = deleteButton.active = refreshButton.active = false; + loginButton.active = deleteButton.active = refreshButton.active = skinsButton.active = false; } } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java index af036c078..976e33ddb 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/Auth.java @@ -34,6 +34,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.mixin.MinecraftClientAccessor; import io.github.axolotlclient.modules.Module; +import io.github.axolotlclient.modules.auth.skin.SkinManager; import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.notifications.Notifications; import io.github.axolotlclient.util.options.GenericOption; @@ -50,14 +51,15 @@ public class Auth extends Accounts implements Module { @Getter private static final Auth Instance = new Auth(); - public final BooleanOption showButton = new BooleanOption("auth.showButton", false); + public final BooleanOption skinManagerAnimations = new BooleanOption("skins.manage.animations", true); private final Minecraft client = Minecraft.getInstance(); private final GenericOption viewAccounts = new GenericOption("viewAccounts", "clickToOpen", () -> client.openScreen(new AccountsScreen(client.screen))); - private final Map textures = new HashMap<>(); private final Set loadingTexture = new HashSet<>(); private final Map profileCache = new WeakHashMap<>(); + @Getter + private final SkinManager skinManager = new SkinManager(); @Override public void init() { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java new file mode 100644 index 000000000..ae10d976a --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -0,0 +1,809 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tessellator; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Color; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ClickableWidget; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.Element; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ParentElement; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.ElementListWidget; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.widgets.VanillaButtonWidget; +import io.github.axolotlclient.bridge.util.AxoText; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.modules.hud.util.DrawUtil; +import io.github.axolotlclient.util.ButtonWidgetTextures; +import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.Watcher; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.render.TextRenderer; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.resource.Identifier; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SkinManagementScreen extends io.github.axolotlclient.AxolotlClientConfig.impl.ui.Screen { + private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); + private static final int LIST_SKIN_WIDTH = 75; + private static final int LIST_SKIN_HEIGHT = 110; + private static final String TEXT_EQUIPPING = I18n.translate("skins.manage.equipping"); + private final Screen parent; + private final Account account; + private MSApi.MCProfile cachedProfile; + private SkinListWidget skinList; + private SkinListWidget capesList; + private boolean capesTab; + private SkinWidget current; + private final Watcher skinDirWatcher; + private final CompletableFuture refreshFuture; + private String tooltip = null; + + public SkinManagementScreen(Screen parent, Account account) { + super(I18n.translate("skins.manage")); + this.parent = parent; + this.account = account; + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + if (account.needsRefresh()) { + refreshFuture = account.refresh(Auth.getInstance().getMsApi()); + } else { + refreshFuture = CompletableFuture.completedFuture(null); + } + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + tooltip = null; + super.render(mouseX, mouseY, delta); + if (tooltip != null) { + renderTooltip(tooltip, mouseX, mouseY+20); + Lighting.turnOff(); + } + } + + @Override + public void init() { + int headerHeight = 33; + int contentHeight = height - headerHeight * 2; + class TextWidget extends ClickableWidget { + + public TextWidget(int x, int y, int width, int height, String message) { + super(x, y, width, height, message); + active = false; + } + + @Override + public void drawWidget(int mouseX, int mouseY, float delta) { + drawCenteredString(textRenderer, getMessage(), getX() + getWidth() / 2, getY() + getHeight() / 2 - textRenderer.fontHeight / 2, -1); + } + } + + var titleWidget = new TextWidget(0, headerHeight / 2 - textRenderer.fontHeight / 2, width, textRenderer.fontHeight, getTitle()); + addDrawableChild(titleWidget); + + var back = addDrawableChild(new VanillaButtonWidget(width / 2 - 75, height - headerHeight / 2 - 10, 150, 20, I18n.translate("gui.back"), btn -> closeScreen())); + + var loadingPlaceholder = new ClickableWidget(0, headerHeight, width, contentHeight, I18n.translate("skins.loading")) { + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int centerX = this.getX() + this.getWidth() / 2; + int centerY = this.getY() + this.getHeight() / 2; + var text = this.getMessage(); + textRenderer.draw(text, centerX - textRenderer.getWidth(text) / 2f, centerY - 9, -1, false); + String string = switch ((int) (System.currentTimeMillis() / 300L % 4L)) { + case 1, 3 -> "o O o"; + case 2 -> "o o O"; + default -> "O o o"; + }; + textRenderer.draw(string, centerX - textRenderer.getWidth(string) / 2f, centerY + 9, 0xFF808080, false); + } + }; + loadingPlaceholder.active = false; + addDrawableChild(loadingPlaceholder); + addDrawableChild(back); + + skinList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, LIST_SKIN_HEIGHT + 34); + capesList = new SkinListWidget(minecraft, width / 2, contentHeight - 24, headerHeight + 24, skinList.getEntryContentsHeight() + 24); + skinList.setLeftPos(width / 2); + capesList.setLeftPos(width / 2); + var currentHeight = Math.min((width / 2f) * 120 / 85, contentHeight); + var currentWidth = currentHeight * 85 / 120; + current = new SkinWidget((int) currentWidth, (int) currentHeight, null, account); + current.setPosition((int) (width / 4f - currentWidth / 2), (int) (height / 2f - currentHeight / 2)); + + if (!capesTab) { + capesList.visible = capesList.active = false; + } else { + skinList.visible = skinList.active = false; + } + List navBar = new ArrayList<>(); + var skinsTab = new VanillaButtonWidget(width * 3 / 4 - 102, headerHeight, 100, 20, I18n.translate("skins.nav.skins"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = true; + capesList.visible = capesList.active = false; + capesTab = false; + }); + navBar.add(skinsTab); + var capesTab = new VanillaButtonWidget(width * 3 / 4 + 2, headerHeight, 100, 20, I18n.translate("skins.nav.capes"), btn -> { + navBar.forEach(w -> { + if (w != btn) w.active = true; + }); + btn.active = false; + skinList.visible = skinList.active = false; + capesList.visible = capesList.active = true; + this.capesTab = true; + }); + navBar.add(capesTab); + skinsTab.active = this.capesTab; + capesTab.active = !this.capesTab; + Runnable addWidgets = () -> { + clearChildren(); + addDrawableChild(titleWidget); + addDrawableChild(current); + addDrawableChild(skinList); + addDrawableChild(capesList); + addDrawableChild(skinsTab); + addDrawableChild(capesTab); + addDrawableChild(back); + }; + if (cachedProfile != null) { + initDisplay(); + addWidgets.run(); + return; + } + refreshFuture.thenComposeAsync(unused -> Auth.getInstance().getMsApi().getProfile(account)) + .thenAcceptAsync(profile -> { + cachedProfile = profile; + initDisplay(); + addWidgets.run(); + }).exceptionally(t -> { + if (t.getCause() instanceof CancellationException) { + minecraft.openScreen(parent); + return null; + } + AxolotlClientCommon.getInstance().getLogger().error("Failed to load skins!", t); + var error = I18n.translate("skins.error.failed_to_load"); + var errorDesc = I18n.translate("skins.error.failed_to_load_desc"); + clearChildren(); + addDrawableChild(titleWidget); + + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error)); + addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(errorDesc) / 2, height / 2 + 1, textRenderer.getWidth(errorDesc), textRenderer.fontHeight, errorDesc)); + addDrawableChild(back); + return null; + }); + } + + private void initDisplay() { + loadSkinsList(); + loadCapesList(); + } + + private void refreshCurrentList() { + if (capesTab) { + var scroll = capesList.getScrollAmount(); + loadCapesList(); + capesList.setScrollAmount(scroll); + } else { + var scroll = skinList.getScrollAmount(); + loadSkinsList(); + skinList.setScrollAmount(scroll); + } + } + + private void loadCapesList() { + capesList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + var capes = profile.capes(); + var deselectCape = createWidgetForCape(current.getSkin(), null); + var activeCape = capes.stream().filter(Cape::active).findFirst(); + current.setCape(activeCape.orElse(null)); + deselectCape.noCape(activeCape.isEmpty()); + for (int i = 0; i < capes.size() + 1; i += columns) { + Entry widget; + if (i == 0) { + widget = createEntry(capesList.getEntryContentsHeight(), deselectCape, I18n.translate("skins.capes.no_cape")); + } else { + var cape = capes.get(i - 1); + widget = createEntryForCape(current.getSkin(), cape, capesList.getEntryContentsHeight()); + } + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < capes.size() + 1 - c)) continue; + var cape2 = capes.get(i + c - 1); + Entry widget2 = createEntryForCape(current.getSkin(), cape2, capesList.getEntryContentsHeight()); + + widgets.add(widget2); + } + capesList.addEntry(new Row(widgets)); + } + } + + private void loadSkinsList() { + skinList.clearEntries(); + var profile = cachedProfile; + int columns = Math.max(2, (width / 2 - 25) / LIST_SKIN_WIDTH); + List skins = new ArrayList<>(profile.skins()); + var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); + var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); + var local = new ArrayList<>(loadLocalSkins()); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + skins.replaceAll(s -> { + if (s instanceof MSApi.MCProfile.OnlineSkin online) { + if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { + local.remove(localHashes.remove(s.textureKey())); + return new Skin.Shared(file, online); + } + } + return s; + }); + skins.addAll(local); + if (!hashes.contains(defaultSkinHash)) { + skins.add(null); + } + populateSkinList(skins, columns); + } + + private List loadLocalSkins() { + try { + Files.createDirectories(SKINS_DIR); + try (Stream skins = Files.list(SKINS_DIR)) { + return skins.filter(Files::isRegularFile).sorted(Comparator.comparingLong(p -> { + try { + return Files.getLastModifiedTime(p).toMillis(); + } catch (IOException e) { + return 0L; + } + }).reversed()).map(Auth.getInstance().getSkinManager()::read).filter(Objects::nonNull).toList(); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to read skins dir!", e); + } + return Collections.emptyList(); + } + + private void populateSkinList(List skins, int columns) { + int entryHeight = skinList.getEntryContentsHeight(); + for (int i = 0; i < skins.size(); i += columns) { + var s = skins.get(i); + if (s != null && s.active()) { + current.setSkin(s); + } + var widget = createEntryForSkin(s, entryHeight); + List widgets = new ArrayList<>(); + widgets.add(widget); + for (int c = 1; c < columns; c++) { + if (!(i < skins.size() - c)) continue; + var s2 = skins.get(i + c); + if (s2 != null && s2.active()) { + current.setSkin(s2); + } + var widget2 = createEntryForSkin(s2, entryHeight); + widgets.add(widget2); + } + skinList.addEntry(new Row(widgets)); + } + } + + /*@Override + public void filesDragged(List packs) { + packs.forEach(p -> { + try { + Files.copy(p, SKINS_DIR.resolve(p.getFileName())); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }); + loadSkinsList(); + }*/ + + private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { + return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); + } + + private @NotNull Entry createEntryForCape(Skin currentSkin, Cape cape, int entryHeight) { + return createEntry(entryHeight, createWidgetForCape(currentSkin, cape), I18n.translate(cape.alias())); + } + + private SkinWidget createWidgetForCape(Skin currentSkin, Cape cape) { + SkinWidget widget2 = new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, currentSkin, cape, account); + widget2.setRotationY(210); + return widget2; + } + + @Override + public void clearAndInit() { + Auth.getInstance().getSkinManager().releaseAll(); + super.clearAndInit(); + } + + @Override + public void removed() { + Auth.getInstance().getSkinManager().releaseAll(); + Watcher.close(skinDirWatcher); + } + + public void closeScreen() { + minecraft.openScreen(parent); + } + + private SkinListWidget getCurrentList() { + return capesTab ? capesList : skinList; + } + + private class SkinListWidget extends ElementListWidget { + public boolean active = true, visible = true; + + public SkinListWidget(Minecraft minecraft, int width, int height, int y, int entryHeight) { + super(minecraft, width, SkinManagementScreen.this.height, y, y + height, entryHeight); + setRenderHeader(false, 0); + setRenderBackground(false); + setRenderHorizontalShadows(false); + } + + @Override + public int addEntry(Row entry) { + return super.addEntry(entry); + } + + @Override + protected int getScrollbarPositionX() { + return right - 8; + } + + @Override + public int getRowLeft() { + return left + 3; + } + + @Override + public int getRowWidth() { + if (!(getMaxScroll() > 0)) { + return width - 4; + } + return width - 14; + } + + public int getEntryContentsHeight() { + return itemHeight - 4; + } + + public void clearEntries() { + super.clearEntries(); + } + + @Override + public void centerScrollOn(Row entry) { + super.centerScrollOn(entry); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amountX, double amountY) { + if (!visible) return false; + return super.mouseScrolled(mouseX, mouseY, amountX, amountY); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return active && visible && super.isMouseOver(mouseX, mouseY); + } + + @Override + public void render(int mouseX, int mouseY, float delta) { + if (!visible) return; + super.render(mouseX, mouseY, delta); + renderGradient(); + } + + private void renderGradient() { + GlStateManager.depthFunc(515); + GlStateManager.disableDepthTest(); + GlStateManager.enableBlend(); + GlStateManager.blendFuncSeparate(770, 771, 0, 1); + GlStateManager.disableAlphaTest(); + GlStateManager.shadeModel(7425); + GlStateManager.disableTexture(); + GlStateManager.enableBlend(); + GlStateManager.disableTexture(); + var tessellator = Tessellator.getInstance(); + var bufferBuilder = tessellator.getBuilder(); + bufferBuilder.begin(7, DefaultVertexFormat.POSITION_TEX_COLOR); + bufferBuilder.vertex(left, top + 4, 0.0F).texture(0.0F, 1.0F).color(0, 0, 0, 0).nextVertex(); + bufferBuilder.vertex(right, top + 4, 0.0F).texture(1.0F, 1.0F).color(0, 0, 0, 0).nextVertex(); + bufferBuilder.vertex(right, top, 0.0F).texture(1.0F, 0.0F).color(0, 0, 0, 255).nextVertex(); + bufferBuilder.vertex(left, top, 0.0F).texture(0.0F, 0.0F).color(0, 0, 0, 255).nextVertex(); + tessellator.end(); + bufferBuilder.begin(7, DefaultVertexFormat.POSITION_TEX_COLOR); + bufferBuilder.vertex(this.left, this.bottom, 0.0F).texture(0.0F, 1.0F).color(0, 0, 0, 255).nextVertex(); + bufferBuilder.vertex(this.right, this.bottom, 0.0F).texture(1.0F, 1.0F).color(0, 0, 0, 255).nextVertex(); + bufferBuilder.vertex(this.right, this.bottom - 4, 0.0F).texture(1.0F, 0.0F).color(0, 0, 0, 0).nextVertex(); + bufferBuilder.vertex(this.left, this.bottom - 4, 0.0F).texture(0.0F, 0.0F).color(0, 0, 0, 0).nextVertex(); + tessellator.end(); + GlStateManager.enableTexture(); + } + } + + private class Row extends ElementListWidget.Entry { + private final List widgets; + + public Row(List entries) { + this.widgets = entries; + } + + @Override + public void render(int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick) { + int x = left; + if (widgets.isEmpty()) return; + int count = widgets.size(); + int padding = ((width - 5 * (count - 1)) / count); + for (var w : widgets) { + w.setPosition(x, top); + w.setWidth(padding); + w.render(mouseX, mouseY, partialTick); + x += w.getWidth() + 5; + } + } + + @Override + public @NotNull List children() { + return widgets; + } + + @Override + public void setFocusedChild(@Nullable Element focused) { + super.setFocusedChild(focused); + if (focused != null) { + getCurrentList().centerScrollOn(this); + } + } + } + + Entry createEntry(int height, SkinWidget widget) { + return createEntry(height, widget, null); + } + + Entry createEntry(int height, SkinWidget widget, String label) { + return new Entry(height, widget, label); + } + + private class Entry extends ClickableWidget implements ParentElement { + private final SkinWidget skinWidget; + private final @Nullable ClickableWidget label; + private final List actionButtons = new ArrayList<>(); + private final ClickableWidget equipButton; + private boolean equipping; + private long equippingStart; + @Nullable + private Element focused; + private boolean dragging; + + public Entry(int height, SkinWidget widget, @Nullable String label) { + super(0, 0, widget.getWidth(), height, ""); + widget.setWidth(getWidth() - 4); + var asset = widget.getFocusedAsset(); + if (asset != null) { + if (asset.isLocal()) { + var delete = new VanillaButtonWidget(0, 0, 11, 11, I18n.translate("skins.manage.delete"), btn -> { + btn.active = false; + client.openScreen(new ConfirmScreen((confirmed, i) -> { + client.openScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); + } + } + btn.active = true; + }, I18n.translate("skins.manage.delete.confirm"), ((Text) (asset.active() ? + AxoText.translatable("skins.manage.delete.confirm.desc_active") : + AxoText.translatable("skins.manage.delete.confirm.desc") + ).br$color(Colors.RED.toInt())).getFormattedString(), 0)); + }) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); + + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int i = 1; + if (!this.active) { + i = 0; + } else if (hovered) { + i = 2; + } + + Identifier tex = ButtonWidgetTextures.get(i); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + minecraft.getTextureManager().bind(SPRITE); + DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { + + } + }; + this.actionButtons.add(delete); + } + if (asset.supportsDownload() && !asset.isLocal()) { + var download = new VanillaButtonWidget(0, 0, 11, 11, I18n.translate("skins.manage.download"), btn -> { + btn.active = false; + asset.image().thenAcceptAsync(b -> { + try { + var out = SKINS_DIR.resolve(asset.textureKey()); + Files.createDirectories(out.getParent()); + Files.write(out, b); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); + } + refreshCurrentList(); + btn.active = true; + }); + }) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int i = 1; + if (!this.active) { + i = 0; + } else if (hovered) { + i = 2; + } + + Identifier tex = ButtonWidgetTextures.get(i); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + minecraft.getTextureManager().bind(SPRITE); + DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { + + } + }; + this.actionButtons.add(download); + } + } + if (label != null) { + this.label = new ClickableWidget(0, 0, widget.getWidth(), 16, label) { + @Override + protected void drawWidget(int mouseX, int mouseY, float partialTick) { + DrawUtil.drawScrollableText(textRenderer, getMessage(), getX() + 2, getY(), getX() + getWidth() - 2, getY() + getHeight(), -1); + } + }; + this.label.active = false; + } else { + this.label = null; + } + this.equipButton = new VanillaButtonWidget(0, 0, widget.getWidth(), 20, I18n.translate( + widget.isEquipped() ? "skins.manage.equipped" : "skins.manage.equip"), + btn -> { + equippingStart = System.currentTimeMillis(); + equipping = true; + btn.setMessage(TEXT_EQUIPPING); + btn.active = false; + widget.equip().thenAcceptAsync(p -> { + cachedProfile = p; + refreshCurrentList(); + }).exceptionally(t -> { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to equip asset!", t); + equipping = false; + return null; + }); + }); + this.equipButton.active = !widget.isEquipped(); + this.skinWidget = widget; + } + + @Override + public final boolean isDragging() { + return this.dragging; + } + + @Override + public final void setDragging(boolean dragging) { + this.dragging = dragging; + } + + @Nullable + @Override + public Element getFocused() { + return this.focused; + } + + @Override + public void setFocusedChild(@Nullable Element child) { + if (this.focused != null) { + this.focused.setFocused(false); + } + + if (child != null) { + child.setFocused(true); + } + + this.focused = child; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + return ParentElement.super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + return ParentElement.super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean isFocused() { + return ParentElement.super.isFocused(); + } + + @Override + public void setFocused(boolean focused) { + ParentElement.super.setFocused(focused); + } + + @Override + public @NotNull List children() { + return Stream.concat(actionButtons.stream(), Stream.of(skinWidget, label, equipButton)).filter(Objects::nonNull).toList(); + } + + private float applyEasing(float x) { + return x * x * x; + } + + @Override + protected void drawWidget(int mouseX, int mouseY, float partialTick) { + int y = getY() + 4; + int x = getX() + 2; + if (skinWidget.isEquipped() || equipping) { + long prog; + if (Auth.getInstance().skinManagerAnimations.get()) { + if (equipping) prog = (System.currentTimeMillis() - equippingStart) / 20 % 100; + else prog = Math.abs((System.currentTimeMillis() / 30 % 200) - 100); + } else prog = 100; + var percent = (prog / 100f); + float gradientWidth; + if (equipping) { + gradientWidth = percent * Math.min(getWidth() / 3f, getHeight() / 3f); + } else { + gradientWidth = Math.min(getWidth() / 15f, getHeight() / 6f) + applyEasing(percent) * Math.min(getWidth() * 2 / 15f, getHeight() / 6f); + } + GradientHoleRectangleRenderState.render(getX() + 2, getY() + 2, getX() + getWidth() - 2, + skinWidget.getY() + skinWidget.getHeight() + 2, + gradientWidth, + equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); + } + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); + skinWidget.render(mouseX, mouseY, partialTick); + int actionButtonY = getY() + 2; + for (var button : actionButtons) { + button.setPosition(skinWidget.getX() + skinWidget.getWidth() - button.getWidth(), actionButtonY); + if (isHovered() || button.isHovered()) { + button.render(mouseX, mouseY, partialTick); + } + if (button.isHovered()) { + //GlStateManager.translatef(0, 0, 200); + tooltip = button.getMessage(); + //GlStateManager.translatef(0, 0, -400); + } + actionButtonY += button.getHeight() + 2; + } + if (label != null) { + label.setPosition(x, skinWidget.getY() + skinWidget.getHeight() + 6); + label.render(mouseX, mouseY, partialTick); + label.setWidth(getWidth() - 4); + equipButton.setPosition(x, label.getY() + label.getHeight() + 2); + } else { + equipButton.setPosition(x, skinWidget.getY() + skinWidget.getHeight() + 4); + } + equipButton.setWidth(getWidth() - 4); + equipButton.render(mouseX, mouseY, partialTick); + + if (isHovered()) { + DrawUtil.outlineRect(getX(), getY(), getWidth(), getHeight(), -1); + } + } + + private static class GradientHoleRectangleRenderState { + + public static void render(int x0, int y0, int x1, int y1, float gradientWidth, int col1, int col2) { + var tess = Tessellator.getInstance(); + var vertexConsumer = tess.getBuilder(); + float z = 0; + int a1 = ClientColors.ARGB.alpha(col1); + int r1 = ClientColors.ARGB.red(col1); + int g1 = ClientColors.ARGB.green(col1); + int b1 = ClientColors.ARGB.blue(col1); + int a2 = ClientColors.ARGB.alpha(col2); + int r2 = ClientColors.ARGB.red(col2); + int g2 = ClientColors.ARGB.green(col2); + int b2 = ClientColors.ARGB.blue(col2); + GlStateManager.disableTexture(); + GlStateManager.enableBlend(); + GlStateManager.disableAlphaTest(); + GlStateManager.blendFuncSeparate(770, 771, 1, 0); + GlStateManager.shadeModel(7425); + //top + vertexConsumer.begin(7, DefaultVertexFormat.POSITION_COLOR); + vertexConsumer.vertex(x0, y0, z).color(r1, g1, b1, a1).nextVertex(); + vertexConsumer.vertex(x0 + gradientWidth, y0 + gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x1 - gradientWidth, y0 + gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x1, y0, z).color(r1, g1, b1, a1).nextVertex(); + //left + vertexConsumer.vertex(x0, y1, z).color(r1, g1, b1, a1).nextVertex(); + vertexConsumer.vertex(x0 + gradientWidth, y1 - gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x0 + gradientWidth, y0 + gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x0, y0, z).color(r1, g1, b1, a1).nextVertex(); + //bottom + vertexConsumer.vertex(x1, y1, z).color(r1, g1, b1, a1).nextVertex(); + vertexConsumer.vertex(x1 - gradientWidth, y1 - gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x0 + gradientWidth, y1 - gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x0, y1, z).color(r1, g1, b1, a1).nextVertex(); + //right + vertexConsumer.vertex(x1, y0, z).color(r1, g1, b1, a1).nextVertex(); + vertexConsumer.vertex(x1 - gradientWidth, y0 + gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x1 - gradientWidth, y1 - gradientWidth, z).color(r2, g2, b2, a2).nextVertex(); + vertexConsumer.vertex(x1, y1, z).color(r1, g1, b1, a1).nextVertex(); + tess.end(); + GlStateManager.shadeModel(7424); + GlStateManager.disableBlend(); + GlStateManager.enableAlphaTest(); + GlStateManager.enableTexture(); + } + } + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java new file mode 100644 index 000000000..c8ad8551c --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -0,0 +1,144 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListSet; + +import com.google.common.hash.Hashing; +import io.github.axolotlclient.AxolotlClientCommon; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.AxoMinecraftClient; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.util.ClientColors; +import net.minecraft.client.Minecraft; +import net.minecraft.client.render.texture.DynamicTexture; +import net.minecraft.client.resource.skin.DefaultSkinUtils; +import net.minecraft.resource.Identifier; + +public class SkinManager { + + private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); + + @SuppressWarnings("UnstableApiUsage") + public Skin read(Path p) { + boolean slim; + String sha256; + try { + var in = Files.readAllBytes(p); + sha256 = Hashing.sha256().hashBytes(in).toString(); + try (var bs = new ByteArrayInputStream(in)) { + var img = ImageIO.read(bs); + slim = (ClientColors.ARGB.alpha(img.getRGB(47, 63)) == 0); + } + return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + } catch (Exception e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + } + return null; + } + + + public CompletableFuture loadSkin(Skin skin) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); + if (loadedTextures.contains(rl)) { + return CompletableFuture.completedFuture(rl); + } + + return skin.image().thenApplyAsync(bytes -> { + try (var bs = new ByteArrayInputStream(bytes)) { + var img = ImageIO.read(bs); + var tex = new DynamicTexture(img.getWidth(), img.getHeight()); + img.getRGB(0, 0, img.getWidth(), img.getHeight(), tex.getPixels(), 0, img.getWidth()); + tex.upload(); + Minecraft.getInstance().getTextureManager().register((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((v, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load skin!", t); + } + return v; + }); + } + + public AxoIdentifier loadCape(Cape cape) { + var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "capes/" + cape.textureKey()); + if (loadedTextures.contains(rl)) { + return rl; + } + + return cape.image().thenApplyAsync(bytes -> { + try (var bs = new ByteArrayInputStream(bytes)) { + var img = ImageIO.read(bs); + var tex = new DynamicTexture(img.getWidth(), img.getHeight()); + img.getRGB(0, 0, img.getWidth(), img.getHeight(), tex.getPixels(), 0, img.getWidth()); + tex.upload(); + Minecraft.getInstance().getTextureManager().register((Identifier) rl, tex); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + loadedTextures.add(rl); + return rl; + }, AxoMinecraftClient.getInstance()).handle((id, t) -> { + if (t != null) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to load cape!", t); + } + return id; + }).getNow(null); + + } + + public void releaseAll() { + loadedTextures.forEach(id -> Minecraft.getInstance().getTextureManager().close((Identifier) id)); + loadedTextures.clear(); + } + + @SuppressWarnings("UnstableApiUsage") + public String getDefaultSkinHash(Account account) { + var skin = DefaultSkinUtils.getDefaultSkin(UUIDHelper.fromUndashed(account.getUuid())); + var mc = Minecraft.getInstance(); + var resourceManager = mc.getResourceManager(); + try { + var res = resourceManager.getResource(skin); + try ( + var in = res.asStream()) { + return Hashing.sha256().hashBytes(in.readAllBytes()).toString(); + } + } catch (IOException ignored) { + } + return null; + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java new file mode 100644 index 000000000..e31261c66 --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -0,0 +1,108 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import com.mojang.blaze3d.platform.GLX; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.render.model.entity.PlayerModel; +import net.minecraft.resource.Identifier; +import org.jetbrains.annotations.Nullable; + +public class SkinRenderer { + private static PlayerModel classicModel, slimModel; + private static final Minecraft minecraft = Minecraft.getInstance(); + + private SkinRenderer() { + } + + public static void render(boolean classicVariant, + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { + + if (classicModel == null && classicVariant) { + classicModel = new PlayerModel(0, false); + classicModel.isBaby = false; + classicModel.setVisible(true); + } + if (slimModel == null && !classicVariant) { + slimModel = new PlayerModel(0, true); + slimModel.isBaby = false; + slimModel.setVisible(true); + } + + int width = x1 - x0; + int light = 15728880; + GLX.multiTexCoord2f(GLX.GL_TEXTURE1, light % 65536, light / 65536f); + GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.pushMatrix(); + GlStateManager.translatef(x0 + width / 2.0F, (float) (y1), 00.0F); + GlStateManager.scalef(scale, scale, 1); + GlStateManager.translatef(0.0F, -0.0625F, 0.0F); + GlStateManager.translatef(0, pivotY, 0); + GlStateManager.rotatef(rotationX, 1, 0, 0); + GlStateManager.translatef(0, -pivotY, 0); + GlStateManager.rotatef(rotationY, 0, 1, 0); + GlStateManager.pushMatrix(); + GlStateManager.scalef(1.0F, 1.0F, -1.0F); + GlStateManager.translatef(0.0F, -1.5F, 0.0F); + var model = classicVariant ? classicModel : slimModel; + minecraft.getTextureManager().bind(skinTexture); + GlStateManager.enableBlend(); + GlStateManager.enableDepthTest(); + GlStateManager.pushMatrix(); + float k = 0.0625F; + model.head.render(k); + model.body.render(k); + model.rightLeg.render(k); + model.leftLeg.render(k); + model.hat.render(k); + model.leftPants.render(k); + model.rightPants.render(k); + model.jacket.render(k); + model.renderLeftArm(); + model.renderRightArm(); + GlStateManager.popMatrix(); + if (cape != null) { + GlStateManager.pushMatrix(); + minecraft.getTextureManager().bind(cape); + GlStateManager.translatef(0.0F, 0.0F, 0.125F); + GlStateManager.rotatef(6.0F, 1, 0, 0); + GlStateManager.rotatef(180.0F, 0, 1, 0); + model.renderCape(0.0625F); + GlStateManager.popMatrix(); + } + GlStateManager.popMatrix(); + GlStateManager.popMatrix(); + GlStateManager.disableBlend(); + GlStateManager.disableDepthTest(); + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java new file mode 100644 index 000000000..6a070f5d8 --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -0,0 +1,130 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.util.concurrent.CompletableFuture; + +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ClickableWidget; +import io.github.axolotlclient.api.util.UUIDHelper; +import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.modules.auth.Account; +import io.github.axolotlclient.modules.auth.Auth; +import io.github.axolotlclient.modules.auth.MSApi; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.client.resource.skin.DefaultSkinUtils; +import net.minecraft.client.sound.system.SoundManager; +import net.minecraft.resource.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class SkinWidget extends ClickableWidget { + private static final float MODEL_HEIGHT = 2.125F; + private static final float FIT_SCALE = 0.97F; + private static final float ROTATION_SENSITIVITY = 2.5F; + private static final float DEFAULT_ROTATION_X = -5.0F; + private static final float DEFAULT_ROTATION_Y = 30.0F; + private static final float ROTATION_X_LIMIT = 50.0F; + private float rotationX = DEFAULT_ROTATION_X; + @Setter + private float rotationY = DEFAULT_ROTATION_Y; + @Getter + @Setter + private Skin skin; + @Getter + @Setter + private Cape cape; + private final Account owner; + private boolean noCape, noCapeActive; + + public SkinWidget(int width, int height, Skin skin, @Nullable Cape cape, Account owner) { + super(0, 0, width, height, ""); + this.skin = skin; + this.cape = cape; + this.owner = owner; + } + + public SkinWidget(int width, int height, Skin skin, Account owner) { + this(width, height, skin, null, owner); + } + + public void noCape(boolean noCapeActive) { + noCape = true; + this.noCapeActive = noCapeActive; + } + + @Override + protected void drawWidget(int mouseX, int mouseY, float partialTick) { + float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; + float pivotY = -1.0625F; + + AxoIdentifier skinRl; + boolean classic; + SkinManager skinManager = Auth.getInstance().getSkinManager(); + CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); + if (loader != null && loader.isDone()) { + skinRl = loader.join(); + classic = skin.isClassicVariant(); + } else { + var uuid = UUIDHelper.fromUndashed(owner.getUuid()); + classic = DefaultSkinUtils.getDefaultModelType(uuid).equals("default"); + skinRl = DefaultSkinUtils.getDefaultSkin(uuid); + } + var capeRl = cape == null ? null : skinManager.loadCape(cape); + + SkinRenderer.render(classic, (Identifier) skinRl, (Identifier) capeRl, this.rotationX, this.rotationY, pivotY, this.getX(), this.getY(), this.getX() + getWidth(), this.getY() + getHeight(), scale); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double dragX, double dragY) { + this.rotationX = MathHelper.clamp(this.rotationX - (float) dragY * ROTATION_SENSITIVITY, -ROTATION_X_LIMIT, ROTATION_X_LIMIT); + this.rotationY += (float) dragX * ROTATION_SENSITIVITY; + } + + @Override + public void playDownSound(SoundManager soundManager) { + + } + + public boolean isEquipped() { + return noCape ? noCapeActive : (cape != null ? cape.active() : skin != null && skin.active()); + } + + public CompletableFuture equip() { + var msApi = Auth.getInstance().getMsApi(); + if (noCape) { + return msApi.hideCape(owner); + } + if (cape != null) { + return cape.equip(msApi, owner); + } + if (skin != null) { + return skin.equip(msApi, owner); + } + return msApi.resetSkin(owner); + } + + public Asset getFocusedAsset() { + return noCape ? null : cape != null ? cape : skin; + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/util/Util.java b/1.8.9/src/main/java/io/github/axolotlclient/util/Util.java index db735461e..a3ad3b689 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/util/Util.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/util/Util.java @@ -239,11 +239,15 @@ public static float lerp(float start, float end, float percent) { } public static Identifier getTexture(GraphicsOption option) { - return getTexture(option.get(), option.getName()); + return getTexture(option.get(), "graphics_"+option.getName()); } public static Identifier getTexture(Graphics graphics, String name) { - Identifier id = new Identifier("axolotlclient", "graphics_" + name.toLowerCase(Locale.ROOT)); + Identifier id = new Identifier("axolotlclient", name.toLowerCase(Locale.ROOT)); + return getTexture(graphics, id); + } + + public static Identifier getTexture(Graphics graphics, Identifier id) { try { DynamicTexture texture; if (Minecraft.getInstance().getTextureManager().get(id) == null) { @@ -262,7 +266,7 @@ public static Identifier getTexture(Graphics graphics, String name) { texture.upload(); } catch (IOException e) { - AxolotlClient.LOGGER.error("Failed to bind texture for " + name + ": ", e); + AxolotlClient.LOGGER.error("Failed to bind texture for " + id + ": ", e); } return id; } diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java index 021c0f651..e07ae64f9 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/Account.java @@ -24,7 +24,6 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import com.google.gson.JsonObject; diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index b19c756e5..9d8014950 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -194,7 +194,7 @@ public static MCProfile get(JsonObject json) { public record OnlineSkin(String id, String state, String url, String variant, String textureKey) implements Skin { public static final String VARIANT_CLASSIC = "CLASSIC"; - public static final String VARIANT_SLIM = "SLIM"; + //public static final String VARIANT_SLIM = "SLIM"; public static final String STATE_ACTIVE = "ACTIVE"; public static OnlineSkin get(JsonObject object) { @@ -225,10 +225,6 @@ public boolean isClassicVariant() { return VARIANT_CLASSIC.equals(variant()); } - public boolean isSlimVariant() { - return VARIANT_SLIM.equals(variant()); - } - public boolean active() { return STATE_ACTIVE.equals(state()); } @@ -304,8 +300,8 @@ private record DisplayClaims(String uhs) { private CompletableFuture authXstsMC(String xblToken) { var body = JsonBuilders.JsonObject.create() .field("Properties", JsonBuilders.JsonObject.create() - .field("SandboxId", "RETAIL")) - .field("UserTokens", JsonBuilders.JsonArray.create().field(xblToken)) + .field("SandboxId", "RETAIL") + .field("UserTokens", JsonBuilders.JsonArray.create().field(xblToken))) .field("RelyingParty", "rp://api.minecraftservices.com/") .field("TokenType", "JWT"); return requestJson(HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(body.asString())).uri(URI.create(XBL_XSTS_AUTH_URL)).build()) diff --git a/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java b/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java index c865353e7..157dc3cc3 100644 --- a/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java +++ b/common/src/main/java/io/github/axolotlclient/util/JsonBuilders.java @@ -1,3 +1,25 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + package io.github.axolotlclient.util; import com.google.gson.JsonElement; diff --git a/gradle.properties b/gradle.properties index ec00b1a87..61b332c9a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,4 +32,4 @@ osl=0.16.3 legacy_lwgjl3=1.2.5+1.8.9 -config=3.0.17 +config=3.0.19 From cdebd30e66b2ddd77743e99f0c2af862b3728446 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 6 Sep 2025 11:47:49 +0200 Subject: [PATCH 12/23] Add import skin button --- .../auth/skin/SkinManagementScreen.java | 23 ++++++++- .../auth/skin/SkinManagementScreen.java | 20 ++++++++ .../auth/skin/SkinManagementScreen.java | 10 +++- .../auth/skin/SkinManagementScreen.java | 10 +++- .../auth/skin/SkinManagementScreen.java | 34 ++++++++++++- common/build.gradle.kts | 1 + .../modules/auth/skin/SkinImportUtil.java | 48 +++++++++++++++++++ .../assets/axolotlclient/lang/en_us.json | 3 +- 8 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index fdc8a476d..71da1e05c 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -159,6 +159,25 @@ protected MutableText getNarrationMessage() { this.capesTab = true; }); navBar.add(capesTab); + var importButton = new ButtonWidget(capesTab.x+capesTab.getWidth()-11, capesTab.y-13, 11, 11, LiteralText.EMPTY, btn -> { + btn.active = false; + SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); + }) { + private final Text tooltip = new TranslatableText("skins.manage.import"); + private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + super.renderButton(graphics, mouseX, mouseY, delta); + client.getTextureManager().bindTexture(sprite); + drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { + renderTooltip(matrices, tooltip, mouseX, mouseY); + } + }; skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -168,6 +187,7 @@ protected MutableText getNarrationMessage() { addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(importButton); addDrawableChild(back); }; if (cachedProfile != null) { @@ -199,7 +219,7 @@ public TextWidget(int x, int y, int width, int height, Text message, TextRendere @Override public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { - drawCenteredText(matrices, textRenderer, getMessage(), x+getWidth()/2, y+getHeight()/2-textRenderer.fontHeight/2, -1); + drawCenteredText(matrices, textRenderer, getMessage(), x + getWidth() / 2, y + getHeight() / 2 - textRenderer.fontHeight / 2, -1); } } addDrawableChild(new TextWidget(width / 2 - textRenderer.getWidth(error) / 2, height / 2 - textRenderer.fontHeight - 2, textRenderer.getWidth(error), textRenderer.fontHeight, error, textRenderer)); @@ -341,6 +361,7 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { + if (packs.isEmpty()) return; packs.forEach(p -> { try { Files.copy(p, SKINS_DIR.resolve(p.getFileName())); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index eec67caf3..041433809 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -152,6 +152,24 @@ protected void updateNarration(NarrationMessageBuilder builder) { this.capesTab = true; }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); + var importButton = new ButtonWidget(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13, 11, 11, Text.translatable("skins.manage.import"), btn -> { + btn.active = false; + SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); + }, Supplier::get) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } + }; + importButton.setTooltip(Tooltip.create(importButton.getMessage())); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -162,6 +180,7 @@ protected void updateNarration(NarrationMessageBuilder builder) { addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(importButton); addDrawableChild(back); }; if (cachedProfile != null) { @@ -312,6 +331,7 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { + if (packs.isEmpty()) return; packs.forEach(p -> { try { Files.copy(p, SKINS_DIR.resolve(p.getFileName())); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index e6648b1d2..a238e69f1 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -145,6 +145,12 @@ protected void init() { this.capesTab = true; }).pos(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); + var importButton = SpriteIconButton.builder(Component.translatable("skins.manage.import"), btn -> { + btn.active = false; + SkinImportUtil.openImportSkinDialog().thenAccept(this::onFilesDrop).thenRun(() -> btn.active = true); + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); + importButton.setTooltip(Tooltip.create(importButton.getMessage())); + importButton.setPosition(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -155,6 +161,7 @@ protected void init() { addRenderableWidget(capesTab); addRenderableWidget(skinList); addRenderableWidget(capesList); + addRenderableWidget(importButton); addRenderableWidget(back); }; if (cachedProfile != null) { @@ -250,7 +257,7 @@ private void loadSkinsList() { } return s; }); - //local.removeIf(s -> hashes.contains(s.textureKey())); + skins.addAll(local); if (!hashes.contains(defaultSkinHash)) { skins.add(null); @@ -301,6 +308,7 @@ private void populateSkinList(List skins, int columns) { @Override public void onFilesDrop(List packs) { + if (packs.isEmpty()) return; packs.forEach(p -> { try { Files.copy(p, SKINS_DIR.resolve(p.getFileName())); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index ce8a46881..7a535a797 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -144,6 +144,12 @@ protected void init() { this.capesTab = true; }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); + var importButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import"), btn -> { + btn.active = false; + SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); + }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); + importButton.setTooltip(Tooltip.create(importButton.getMessage())); + importButton.setPosition(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -154,6 +160,7 @@ protected void init() { addDrawableSelectableElement(capesTab); addDrawableSelectableElement(skinList); addDrawableSelectableElement(capesList); + addDrawableSelectableElement(importButton); addDrawableSelectableElement(back); }; if (cachedProfile != null) { @@ -248,7 +255,7 @@ private void loadSkinsList() { } return s; }); - //local.removeIf(s -> hashes.contains(s.textureKey())); + skins.addAll(local); if (!hashes.contains(defaultSkinHash)) { skins.add(null); @@ -299,6 +306,7 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { + if (packs.isEmpty()) return; packs.forEach(p -> { try { Files.copy(p, SKINS_DIR.resolve(p.getFileName())); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index ae10d976a..c4ad51973 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -62,6 +62,7 @@ import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.lwjgl.util.tinyfd.TinyFileDialogs; public class SkinManagementScreen extends io.github.axolotlclient.AxolotlClientConfig.impl.ui.Screen { private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); @@ -177,6 +178,33 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { this.capesTab = true; }); navBar.add(capesTab); + var importButton = new VanillaButtonWidget(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13, 11, 11, I18n.translate("skins.manage.import"), btn -> { + btn.active = false; + SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); + }) { + private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); + + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int i = 1; + if (!this.active) { + i = 0; + } else if (hovered) { + tooltip = getMessage(); + i = 2; + } + + Identifier tex = ButtonWidgetTextures.get(i); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + minecraft.getTextureManager().bind(SPRITE); + DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { + + } + }; skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -187,6 +215,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(importButton); addDrawableChild(back); }; if (cachedProfile != null) { @@ -330,8 +359,9 @@ private void populateSkinList(List skins, int columns) { } } - /*@Override + //@Override public void filesDragged(List packs) { + if (packs.isEmpty()) return; packs.forEach(p -> { try { Files.copy(p, SKINS_DIR.resolve(p.getFileName())); @@ -340,7 +370,7 @@ public void filesDragged(List packs) { } }); loadSkinsList(); - }*/ + } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { return createEntry(entryHeight, new SkinWidget(LIST_SKIN_WIDTH, LIST_SKIN_HEIGHT, skin, account)); diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 3cd33558a..2404ebeba 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { testRuntimeOnly(compileOnly("org.apache.commons:commons-lang3:3.3.2")!!) testRuntimeOnly(compileOnly("it.unimi.dsi:fastutil:8.2.1")!!) testRuntimeOnly(compileOnly("org.lwjgl:lwjgl-glfw:3.3.2")!!) + testRuntimeOnly(compileOnly("org.lwjgl:lwjgl-tinyfd:3.2.2")!!) shadow(implementation("io.github.CDAGaming:DiscordIPC:0.10.2") { isTransitive = false diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java new file mode 100644 index 000000000..421d11bee --- /dev/null +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.modules.auth.skin; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import net.fabricmc.loader.api.FabricLoader; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.util.tinyfd.TinyFileDialogs; + +public class SkinImportUtil { + public static CompletableFuture> openImportSkinDialog() { + return CompletableFuture.supplyAsync(() -> { + try (MemoryStack stack = MemoryStack.stackPush()) { + var pointers = stack.pointers(stack.UTF8("*.png")); + var result = TinyFileDialogs.tinyfd_openFileDialog("Import Skins", FabricLoader.getInstance().getGameDir().toString(), pointers, null, true); + if (result != null) { + return Arrays.stream(result.split("\\|")) + .map(Path::of).toList(); + } + return List.of(); + } + }); + } +} diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index a4b65551c..53fe6ab68 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -827,5 +827,6 @@ "skins.manage.delete.confirm.desc": "This Skin's file will be deleted permanently!\n Are you sure?", "skins.manage.delete.confirm.desc_active": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", "skins.manage.animations": "Skin Manger Animations", - "skins.manage.download": "Download Skin" + "skins.manage.download": "Download Skin", + "skins.manage.import": "Import Skin" } From b39458efec83459313fd19b996d8bede0a326de4 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 6 Sep 2025 14:41:14 +0200 Subject: [PATCH 13/23] fix some bugs --- .../auth/skin/SkinManagementScreen.java | 48 +++++++++---------- .../modules/auth/skin/SkinManager.java | 9 +++- .../auth/skin/SkinManagementScreen.java | 1 - .../modules/auth/skin/SkinRenderer.java | 24 +++++----- 1.21.7/build.gradle.kts | 5 -- .../auth/skin/SkinManagementScreen.java | 1 - .../modules/auth/skin/SkinImportUtil.java | 2 +- gradle.properties | 2 +- 8 files changed, 45 insertions(+), 47 deletions(-) diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 71da1e05c..f9941f1c5 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -39,6 +39,7 @@ import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.modules.hud.util.DrawUtil; +import io.github.axolotlclient.util.ButtonWidgetTextures; import io.github.axolotlclient.util.ClientColors; import io.github.axolotlclient.util.Watcher; import net.fabricmc.loader.api.FabricLoader; @@ -80,6 +81,7 @@ public class SkinManagementScreen extends Screen { private final Watcher skinDirWatcher; private final List drawables = new ArrayList<>(); private final CompletableFuture refreshFuture; + private Text tooltip; public SkinManagementScreen(Screen parent, Account account) { super(new TranslatableText("skins.manage")); @@ -159,23 +161,21 @@ protected MutableText getNarrationMessage() { this.capesTab = true; }); navBar.add(capesTab); - var importButton = new ButtonWidget(capesTab.x+capesTab.getWidth()-11, capesTab.y-13, 11, 11, LiteralText.EMPTY, btn -> { + var importButton = new ButtonWidget(capesTab.x+capesTab.getWidth()-11, capesTab.y-13, 11, 11, new TranslatableText("skins.manage.import"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }) { - private final Text tooltip = new TranslatableText("skins.manage.import"); private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); @Override public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - super.renderButton(graphics, mouseX, mouseY, delta); + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); client.getTextureManager().bindTexture(sprite); drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { - renderTooltip(matrices, tooltip, mouseX, mouseY); + if (this.isHovered()) { + tooltip = getMessage(); + } } }; skinsTab.active = this.capesTab; @@ -241,9 +241,13 @@ private void clear() { @Override public void render(MatrixStack graphics, int mouseX, int mouseY, float delta) { + tooltip = null; renderBackground(graphics); drawables.forEach(d -> d.render(graphics, mouseX, mouseY, delta)); drawCenteredText(graphics, textRenderer, getTitle(), width / 2, 33 / 2 - textRenderer.fontHeight / 2, -1); + if (tooltip != null) { + renderTooltip(graphics, tooltip, mouseX, mouseY+20); + } } private void initDisplay() { @@ -535,7 +539,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { var asset = widget.getFocusedAsset(); if (asset != null) { if (asset.isLocal()) { - var delete = new ButtonWidget(0, 0, 11, 11, LiteralText.EMPTY, btn -> { + var delete = new ButtonWidget(0, 0, 11, 11, new TranslatableText("skins.manage.delete"), btn -> { btn.active = false; client.openScreen(new ConfirmScreen(confirmed -> { client.openScreen(SkinManagementScreen.this); @@ -554,25 +558,23 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { ).br$color(Colors.RED.toInt()))); }) { - private final Text tooltip = new TranslatableText("skins.manage.delete"); private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); @Override public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - super.renderButton(graphics, mouseX, mouseY, delta); + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); client.getTextureManager().bindTexture(sprite); drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { - renderTooltip(matrices, tooltip, mouseX, mouseY); + if (this.isHovered()) { + tooltip = getMessage(); + } } }; this.actionButtons.add(delete); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = new ButtonWidget(0, 0, 11, 11, LiteralText.EMPTY, btn -> { + var download = new ButtonWidget(0, 0, 11, 11, new TranslatableText("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -586,19 +588,17 @@ public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { btn.active = true; }); }) { - private final Text tooltip = new TranslatableText("skins.manage.download"); private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); @Override public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - super.renderButton(graphics, mouseX, mouseY, delta); + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); client.getTextureManager().bindTexture(sprite); drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void renderToolTip(MatrixStack matrices, int mouseX, int mouseY) { - renderTooltip(matrices, tooltip, mouseX, mouseY); + if (this.isHovered()) { + tooltip = getMessage(); + } } }; this.actionButtons.add(download); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 893c3be9c..e5a8e7604 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -57,8 +57,13 @@ public Skin read(Path p) { try { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); - try (var img = NativeImage.read(ByteBuffer.wrap(in))) { - slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + try (MemoryStack memoryStack = MemoryStack.stackPush()) { + ByteBuffer byteBuffer = memoryStack.malloc(in.length); + byteBuffer.put(in); + byteBuffer.rewind(); + try (var img = NativeImage.read(byteBuffer)) { + slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + } } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); } catch (Exception e) { diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 041433809..48e8ab27f 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -383,7 +383,6 @@ private class SkinListWidget extends ElementListWidget { public SkinListWidget(MinecraftClient minecraft, int width, int height, int y, int entryHeight) { super(minecraft, width, SkinManagementScreen.this.height, y, y + height, entryHeight); setRenderHeader(false, 0); - setRenderBackground(false); } @Override diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 50f2ed31a..4ffe9fd88 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -42,16 +42,16 @@ private SkinRenderer() { } public static void render(GuiGraphics graphics, boolean classicVariant, - Identifier skinTexture, - @Nullable Identifier cape, - float rotationX, - float rotationY, - float pivotY, - int x0, - int y0, - int x1, - int y1, - float scale) { + Identifier skinTexture, + @Nullable Identifier cape, + float rotationX, + float rotationY, + float pivotY, + int x0, + int y0, + int x1, + int y1, + float scale) { if (classicModel == null && classicVariant) { classicModel = new PlayerEntityModel<>(minecraft.getEntityModelLoader().getModelPart(EntityModelLayers.PLAYER), false); classicModel.child = false; @@ -74,13 +74,13 @@ public static void render(GuiGraphics graphics, boolean classicVariant, graphics.getMatrices().scale(1.0F, 1.0F, -1.0F); graphics.getMatrices().translate(0.0F, -1.5F, 0.0F); var model = classicVariant ? classicModel : slimModel; - RenderLayer renderLayer = model.getLayer(skinTexture); + RenderLayer renderLayer = RenderLayer.getEntityAlpha(skinTexture); model.render(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(renderLayer), LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); if (cape != null) { graphics.getMatrices().translate(0.0F, 0.0F, 0.125F); graphics.getMatrices().multiply(Axis.X_POSITIVE.rotationDegrees(6.0F)); graphics.getMatrices().multiply(Axis.Y_POSITIVE.rotationDegrees(180.0F)); - model.renderCape(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(RenderLayer.getEntitySolid(cape)), LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV); + model.renderCape(graphics.getMatrices(), graphics.getVertexConsumers().getBuffer(RenderLayer.getEntityAlpha(cape)), LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV); } graphics.getMatrices().pop(); graphics.draw(); diff --git a/1.21.7/build.gradle.kts b/1.21.7/build.gradle.kts index 49f02d8a1..e3545f526 100644 --- a/1.21.7/build.gradle.kts +++ b/1.21.7/build.gradle.kts @@ -25,11 +25,6 @@ loom { sourceSet("test") } } - /*runs { - getByName("client") { - vmArg("-XX:+AllowEnhancedClassRedefinition -XX:+IgnoreUnrecognizedVMOptions") - } - }*/ } repositories { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index c4ad51973..f42cc8ab0 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -62,7 +62,6 @@ import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.lwjgl.util.tinyfd.TinyFileDialogs; public class SkinManagementScreen extends io.github.axolotlclient.AxolotlClientConfig.impl.ui.Screen { private static final Path SKINS_DIR = FabricLoader.getInstance().getGameDir().resolve("skins"); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java index 421d11bee..39c4fe63a 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java @@ -39,7 +39,7 @@ public static CompletableFuture> openImportSkinDialog() { var result = TinyFileDialogs.tinyfd_openFileDialog("Import Skins", FabricLoader.getInstance().getGameDir().toString(), pointers, null, true); if (result != null) { return Arrays.stream(result.split("\\|")) - .map(Path::of).toList(); + .map(Path::of).peek(System.out::println).toList(); } return List.of(); } diff --git a/gradle.properties b/gradle.properties index 61b332c9a..a6fda7db5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ fabric.loom.disableMinecraftVerification=true axolotlclient.modules.all=true # Mod Properties -version=3.1.6-alpha.1+skins +version=3.1.6-alpha.1 maven_group=io.github.axolotlclient From 5658749eaf16bf480a86507476613279a6f08485 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sat, 6 Sep 2025 20:02:16 +0200 Subject: [PATCH 14/23] filter out malformed skin images --- .../github/axolotlclient/modules/auth/skin/SkinManager.java | 1 + .../github/axolotlclient/modules/auth/skin/SkinManager.java | 1 + .../github/axolotlclient/modules/auth/skin/SkinManager.java | 1 + .../github/axolotlclient/modules/auth/skin/SkinManager.java | 3 ++- .../modules/auth/skin/SkinManagementScreen.java | 4 ++-- .../github/axolotlclient/modules/auth/skin/SkinManager.java | 1 + .../axolotlclient/modules/auth/skin/SkinImportUtil.java | 5 +++-- 7 files changed, 11 insertions(+), 5 deletions(-) diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index e5a8e7604..c29158513 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -62,6 +62,7 @@ public Skin read(Path p) { byteBuffer.put(in); byteBuffer.rewind(); try (var img = NativeImage.read(byteBuffer)) { + if (img.getWidth() != 64 || img.getHeight() != 64) return null; slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 10f58a56b..2e9daa0d6 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -55,6 +55,7 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { + if (img.getWidth() != 64 || img.getHeight() != 64) return null; slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 4ee94d8eb..6b5d51131 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -55,6 +55,7 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { + if (img.getWidth() != 64 || img.getHeight() != 64) return null; slim = ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0; } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index f115d88ea..eca750a52 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -55,7 +55,8 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { - slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + if (img.getWidth() != 64 || img.getHeight() != 64) return null; + slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); } catch (Exception e) { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index f42cc8ab0..a73a924c9 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -96,7 +96,7 @@ public void render(int mouseX, int mouseY, float delta) { tooltip = null; super.render(mouseX, mouseY, delta); if (tooltip != null) { - renderTooltip(tooltip, mouseX, mouseY+20); + renderTooltip(tooltip, mouseX, mouseY + 20); Lighting.turnOff(); } } @@ -177,7 +177,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { this.capesTab = true; }); navBar.add(capesTab); - var importButton = new VanillaButtonWidget(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13, 11, 11, I18n.translate("skins.manage.import"), btn -> { + var importButton = new VanillaButtonWidget(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13, 11, 11, I18n.translate("skins.manage.import"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }) { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index c8ad8551c..16e547bdf 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -58,6 +58,7 @@ public Skin read(Path p) { sha256 = Hashing.sha256().hashBytes(in).toString(); try (var bs = new ByteArrayInputStream(in)) { var img = ImageIO.read(bs); + if (img.getWidth() != 64 || img.getHeight() != 64) return null; slim = (ClientColors.ARGB.alpha(img.getRGB(47, 63)) == 0); } return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java index 39c4fe63a..a2a8fc8e4 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinImportUtil.java @@ -36,10 +36,11 @@ public static CompletableFuture> openImportSkinDialog() { return CompletableFuture.supplyAsync(() -> { try (MemoryStack stack = MemoryStack.stackPush()) { var pointers = stack.pointers(stack.UTF8("*.png")); - var result = TinyFileDialogs.tinyfd_openFileDialog("Import Skins", FabricLoader.getInstance().getGameDir().toString(), pointers, null, true); + @SuppressWarnings("DataFlowIssue") var result = TinyFileDialogs.tinyfd_openFileDialog("Import Skins", + FabricLoader.getInstance().getGameDir().toString(), pointers, null, true); if (result != null) { return Arrays.stream(result.split("\\|")) - .map(Path::of).peek(System.out::println).toList(); + .map(Path::of).toList(); } return List.of(); } From 273f51f4968de767ba5aacebe72a64f721e2d58c Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 3 Aug 2025 18:38:56 +0200 Subject: [PATCH 15/23] (1.8.9) Add skin file drag'n'drop support improve image clipboard handling --- .../auth/skin/SkinManagementScreen.java | 3 +- .../auth/skin/SkinManagementScreen.java | 3 +- .../modules/hud/HudEditScreen.java | 10 +- .../auth/skin/SkinManagementScreen.java | 3 +- .../modules/hud/HudEditScreen.java | 10 +- .../auth/skin/SkinManagementScreen.java | 3 +- .../modules/hud/HudEditScreen.java | 10 +- 1.8.9/build.gradle.kts | 6 +- .../config/AxolotlClientConfig.java | 16 +- .../mixin/GameRendererMixin.java | 5 +- .../auth/skin/SkinManagementScreen.java | 9 +- .../modules/hud/HudEditScreen.java | 14 +- .../screenshotUtils/ScreenshotUtils.java | 5 +- .../github/axolotlclient/util/GLFWUtil.java | 73 ------ .../axolotlclient/util/WindowAccess.java | 222 ++++++++++++++++++ 1.8.9/src/main/resources/fabric.mod.json | 3 +- build.gradle.kts | 1 + common/build.gradle.kts | 1 + .../screenshotUtils/ScreenshotCopying.java | 93 +++++--- gradle.properties | 2 +- 20 files changed, 352 insertions(+), 140 deletions(-) delete mode 100644 1.8.9/src/main/java/io/github/axolotlclient/util/GLFWUtil.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/util/WindowAccess.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index f9941f1c5..c97f4b47d 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -305,7 +305,8 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity(), (skin, skin2) -> skin)); + local.removeIf(s -> !localHashes.containsValue(s)); skins.replaceAll(s -> { if (s instanceof MSApi.MCProfile.OnlineSkin online) { if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 48e8ab27f..da3d50707 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -271,7 +271,8 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity(), (skin, skin2) -> skin)); + local.removeIf(s -> !localHashes.containsValue(s)); skins.replaceAll(s -> { if (s instanceof MSApi.MCProfile.OnlineSkin online) { if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java index 0959e1411..6b5a38ccd 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java @@ -54,9 +54,9 @@ public class HudEditScreen extends Screen { private static final BooleanOption snapping = new BooleanOption("snapping", true); private static final OptionCategory hudEditScreenCategory = OptionCategory.create("hudEditScreen"); private static final int GRAB_TOLERANCE = 5; - private static final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); - private static final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); - private static final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), + private final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); + private final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); + private final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), NESW_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NESW_CURSOR); public static boolean isSnappingEnabled() { @@ -188,6 +188,10 @@ public void removed() { setCursor(DEFAULT_CURSOR); mode = ModificationMode.NONE; super.removed(); + GLFW.glfwDestroyCursor(MOVE_CURSOR); + GLFW.glfwDestroyCursor(DEFAULT_CURSOR); + GLFW.glfwDestroyCursor(NESW_RESIZE_CURSOR); + GLFW.glfwDestroyCursor(NWSE_RESIZE_CURSOR); } @Override diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index a238e69f1..2401f76a9 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -247,7 +247,8 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity(), (skin, skin2) -> skin)); + local.removeIf(s -> !localHashes.containsValue(s)); skins.replaceAll(s -> { if (s instanceof MSApi.MCProfile.OnlineSkin online) { if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java index 3e3b95182..e8fb14a24 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java @@ -54,9 +54,9 @@ public class HudEditScreen extends Screen { private static final BooleanOption snapping = new BooleanOption("snapping", true); private static final OptionCategory hudEditScreenCategory = OptionCategory.create("hudEditScreen"); private static final int GRAB_TOLERANCE = 5; - private static final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); - private static final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); - private static final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), + private final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); + private final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); + private final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), NESW_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NESW_CURSOR); public static boolean isSnappingEnabled() { @@ -157,6 +157,10 @@ public void removed() { setCursor(DEFAULT_CURSOR); mode = ModificationMode.NONE; super.removed(); + GLFW.glfwDestroyCursor(MOVE_CURSOR); + GLFW.glfwDestroyCursor(DEFAULT_CURSOR); + GLFW.glfwDestroyCursor(NESW_RESIZE_CURSOR); + GLFW.glfwDestroyCursor(NWSE_RESIZE_CURSOR); } @Override diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 7a535a797..b1f206548 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -245,7 +245,8 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity(), (skin, skin2) -> skin)); + local.removeIf(s -> !localHashes.containsValue(s)); skins.replaceAll(s -> { if (s instanceof MSApi.MCProfile.OnlineSkin online) { if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java index 0ea711b2d..33d3b7b21 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java @@ -54,9 +54,9 @@ public class HudEditScreen extends Screen { private static final BooleanOption snapping = new BooleanOption("snapping", true); private static final OptionCategory hudEditScreenCategory = OptionCategory.create("hudEditScreen"); private static final int GRAB_TOLERANCE = 5; - private static final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); - private static final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); - private static final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), + private final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); + private final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); + private final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), NESW_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NESW_CURSOR); public static boolean isSnappingEnabled() { @@ -157,6 +157,10 @@ public void removed() { setCursor(DEFAULT_CURSOR); mode = ModificationMode.NONE; super.removed(); + GLFW.glfwDestroyCursor(MOVE_CURSOR); + GLFW.glfwDestroyCursor(DEFAULT_CURSOR); + GLFW.glfwDestroyCursor(NESW_RESIZE_CURSOR); + GLFW.glfwDestroyCursor(NWSE_RESIZE_CURSOR); } @Override diff --git a/1.8.9/build.gradle.kts b/1.8.9/build.gradle.kts index 02e6869d5..7a82c8912 100644 --- a/1.8.9/build.gradle.kts +++ b/1.8.9/build.gradle.kts @@ -59,9 +59,10 @@ dependencies { localRuntime("org.slf4j:slf4j-jdk14:1.7.36") compileOnly("org.lwjgl:lwjgl-glfw:${lwjglVersion}") + compileOnly("org.lwjgl:lwjgl-sdl:3.4.0-SNAPSHOT") - modCompileOnly("io.github.moehreag:legacy-lwjgl3:${project.property("legacy_lwgjl3")}") - modLocalRuntime("io.github.moehreag:legacy-lwjgl3:${project.property("legacy_lwgjl3")}:all-remapped") + modImplementation("io.github.moehreag:legacy-lwjgl3:${project.property("legacy_lwgjl3")}") + //modLocalRuntime("io.github.moehreag:legacy-lwjgl3:${project.property("legacy_lwgjl3")}:all-remapped") include(implementation("org.lwjgl", "lwjgl-tinyfd", lwjglVersion)) include(runtimeOnly("org.lwjgl", "lwjgl-tinyfd", lwjglVersion, classifier = "natives-linux")) @@ -79,6 +80,7 @@ dependencies { } configurations.configureEach { + exclude("org.lwjgl.lwjgl") resolutionStrategy { dependencySubstitution { substitute(module("io.netty:netty-all:4.0.23.Final")).using(module("io.netty:netty-all:4.0.56.Final")) diff --git a/1.8.9/src/main/java/io/github/axolotlclient/config/AxolotlClientConfig.java b/1.8.9/src/main/java/io/github/axolotlclient/config/AxolotlClientConfig.java index d52662c5e..263ea8094 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/config/AxolotlClientConfig.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/config/AxolotlClientConfig.java @@ -36,7 +36,7 @@ import io.github.axolotlclient.config.screen.CreditsScreen; import io.github.axolotlclient.config.screen.ProfilesScreen; import io.github.axolotlclient.modules.Module; -import io.github.axolotlclient.util.GLFWUtil; +import io.github.axolotlclient.util.WindowAccess; import io.github.axolotlclient.util.options.ForceableBooleanOption; import io.github.axolotlclient.util.options.GenericOption; import lombok.Getter; @@ -44,7 +44,6 @@ import net.minecraft.client.options.KeyBinding; import net.ornithemc.osl.keybinds.api.KeyBindingEvents; import net.ornithemc.osl.lifecycle.api.client.MinecraftClientEvents; -import org.lwjgl.glfw.GLFW; public class AxolotlClientConfig extends AxolotlClientConfigCommon { @@ -73,7 +72,7 @@ public class AxolotlClientConfig extends AxolotlClientConfigCommon { public final ColorOption loadingScreenColor = new ColorOption("loadingBgColor", new Color(-1)); public final BooleanOption nightMode = new BooleanOption("nightMode", false); public final BooleanOption rawMouseInput = new BooleanOption("rawMouseInput", false, v -> - GLFWUtil.runUsingGlfwHandle(h -> GLFW.glfwSetInputMode(h, GLFW.GLFW_RAW_MOUSE_MOTION, v ? 1 : 0))); + WindowAccess.getInstance().setRawMouseMotion(v)); public final BooleanOption enableCustomOutlines = new BooleanOption("enabled", false); public final ColorOption outlineColor = new ColorOption("color", Color.parse("#DD000000")); @@ -175,10 +174,15 @@ public AxolotlClientConfig() { AxolotlClient.getInstance().modules.add(new Module() { @Override public void lateInit() { - if (System.getProperty("org.lwjgl.input.Mouse.disableRawInput") == null) { - System.setProperty("org.lwjgl.input.Mouse.disableRawInput", "true"); + if (WindowAccess.getInstance().rawMouseMotionAvailable()) { + + if (System.getProperty("org.lwjgl.input.Mouse.disableRawInput") == null) { + System.setProperty("org.lwjgl.input.Mouse.disableRawInput", "true"); + } + WindowAccess.getInstance().setRawMouseMotion(rawMouseInput.get()); + } else { + AxolotlClient.getInstance().getConfigManager().suppressName(rawMouseInput.getName()); } - GLFWUtil.runUsingGlfwHandle(h -> GLFW.glfwSetInputMode(h, GLFW.GLFW_RAW_MOUSE_MOTION, rawMouseInput.get() ? 1 : 0)); } }); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java b/1.8.9/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java index 81991cfb8..d9ad28e83 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/mixin/GameRendererMixin.java @@ -58,7 +58,6 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.At.Shift; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -94,7 +93,7 @@ public abstract class GameRendererMixin { this.viewDistance = (float) (this.viewDistance * 2 + Minecraft.getInstance().player.getSourcePos().y); Entity entity = this.minecraft.getCamera(); - GL11.glFog(2918, this.setFogColor(this.fogRed, this.fogGreen, this.fogBlue, 1.0F)); + GL11.glFogfv(2918, this.setFogColor(this.fogRed, this.fogGreen, this.fogBlue, 1.0F)); GL11.glNormal3f(0.0F, -1.0F, 0.0F); GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F); Block block = Camera.getBlockInside(this.minecraft.world, entity, tickDelta); @@ -197,7 +196,7 @@ public abstract class GameRendererMixin { } } - @Inject(method = "render(FJ)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/pipeline/RenderTarget;bindWrite(Z)V", shift = Shift.BEFORE)) + @Inject(method = "render(FJ)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/pipeline/RenderTarget;bindWrite(Z)V")) public void axolotlclient$worldMotionBlur(float tickDelta, long nanoTime, CallbackInfo ci) { MenuBlur.getInstance().updateBlur(); axolotlclient$postRender(tickDelta, nanoTime, null); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index a73a924c9..d9e3839fa 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -179,7 +179,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { navBar.add(capesTab); var importButton = new VanillaButtonWidget(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13, 11, 11, I18n.translate("skins.manage.import"), btn -> { btn.active = false; - SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); + SkinImportUtil.openImportSkinDialog().thenAccept(this::onFileDrop).thenRun(() -> btn.active = true); }) { private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); @@ -300,7 +300,8 @@ private void loadSkinsList() { var hashes = skins.stream().map(Asset::textureKey).collect(Collectors.toSet()); var defaultSkinHash = Auth.getInstance().getSkinManager().getDefaultSkinHash(account); var local = new ArrayList<>(loadLocalSkins()); - var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity())); + var localHashes = local.stream().collect(Collectors.toMap(Asset::textureKey, Function.identity(), (skin, skin2) -> skin)); + local.removeIf(s -> !localHashes.containsValue(s)); skins.replaceAll(s -> { if (s instanceof MSApi.MCProfile.OnlineSkin online) { if (localHashes.containsKey(s.textureKey()) && localHashes.get(s.textureKey()) instanceof Skin.Local file) { @@ -358,8 +359,8 @@ private void populateSkinList(List skins, int columns) { } } - //@Override - public void filesDragged(List packs) { + @Override + public void onFileDrop(List packs) { if (packs.isEmpty()) return; packs.forEach(p -> { try { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java index 6b62c2429..71a514923 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/hud/HudEditScreen.java @@ -36,12 +36,11 @@ import io.github.axolotlclient.modules.hud.snapping.SnappingHelper; import io.github.axolotlclient.modules.hud.util.DrawPosition; import io.github.axolotlclient.modules.hud.util.Rectangle; -import io.github.axolotlclient.util.GLFWUtil; +import io.github.axolotlclient.util.WindowAccess; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.resource.language.I18n; -import org.lwjgl.glfw.GLFW; /** * This implementation of Hud modules is based on KronHUD. @@ -55,10 +54,10 @@ public class HudEditScreen extends Screen { private static final BooleanOption snapping = new BooleanOption("snapping", true); private static final OptionCategory hudEditScreenCategory = OptionCategory.create("hudEditScreen"); private static final int GRAB_TOLERANCE = 5; - private static final long MOVE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_ALL_CURSOR); - private static final long DEFAULT_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_ARROW_CURSOR); - private static final long NWSE_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NWSE_CURSOR), - NESW_RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_RESIZE_NESW_CURSOR); + private final long MOVE_CURSOR = WindowAccess.getInstance().createCursor(WindowAccess.Cursor.RESIZE_ALL); + private final long DEFAULT_CURSOR = WindowAccess.getInstance().createCursor(WindowAccess.Cursor.ARROW); + private final long NWSE_RESIZE_CURSOR = WindowAccess.getInstance().createCursor(WindowAccess.Cursor.RESIZE_NWSE), + NESW_RESIZE_CURSOR = WindowAccess.getInstance().createCursor(WindowAccess.Cursor.RESIZE_NESW); public static boolean isSnappingEnabled() { return snapping.get(); @@ -106,7 +105,7 @@ private void updateSnapState() { private void setCursor(long cursor) { if (cursor > 0 && cursor != currentCursor) { currentCursor = cursor; - GLFWUtil.runUsingGlfwHandle(ctx -> GLFW.glfwSetCursor(ctx, cursor)); + WindowAccess.getInstance().setCursor(cursor); } } @@ -287,6 +286,7 @@ public void removed() { super.removed(); setCursor(DEFAULT_CURSOR); mode = ModificationMode.NONE; + WindowAccess.getInstance().destroyCursors(DEFAULT_CURSOR, NWSE_RESIZE_CURSOR, NESW_RESIZE_CURSOR, MOVE_CURSOR); } @Override diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotUtils.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotUtils.java index c6e6e9548..96b58f5df 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotUtils.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotUtils.java @@ -54,9 +54,8 @@ public class ScreenshotUtils extends AbstractModule { private static final ScreenshotUtils Instance = new ScreenshotUtils(); private final OptionCategory category = OptionCategory.create("screenshotUtils"); private final BooleanOption enabled = new BooleanOption("enabled", false); - private final GenericOption openViewer = new GenericOption("imageViewer", "openViewer", () -> { - Minecraft.getInstance().openScreen(new GalleryScreen(Minecraft.getInstance().screen)); - }); + private final GenericOption openViewer = new GenericOption("imageViewer", "openViewer", + () -> Minecraft.getInstance().openScreen(new GalleryScreen(Minecraft.getInstance().screen))); private final Map actions = new LinkedHashMap<>(); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/util/GLFWUtil.java b/1.8.9/src/main/java/io/github/axolotlclient/util/GLFWUtil.java deleted file mode 100644 index 797ab0162..000000000 --- a/1.8.9/src/main/java/io/github/axolotlclient/util/GLFWUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright © 2025 moehreag & Contributors - * - * This file is part of AxolotlClient. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * For more information, see the LICENSE file. - */ - -package io.github.axolotlclient.util; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.function.Consumer; - -import net.ornithemc.osl.lifecycle.api.client.MinecraftClientEvents; -import org.lwjgl.opengl.Display; - -public class GLFWUtil { - - private static MethodHandle getHandle; - - static { - try { - getHandle = MethodHandles.lookup().findStatic(Class.forName("org.lwjgl.opengl.Display"), "getHandle", MethodType.methodType(long.class)); - } catch (Throwable ignored) { - } - } - - private static long windowHandle = -1; - - public static long getWindowHandle() { - if (windowHandle == -1) { - try { - windowHandle = (long) getHandle.invoke(); - } catch (Throwable ignored) { - } - } - return windowHandle; - } - - // Since the reflection used for this only works with legacy-lwjgl3 it's possible for us not being able to access the window handle. - // This however should not lead to a crash despite compiling against glfw. - public static boolean isHandleAvailable() { - return getWindowHandle() != -1; - } - - public static void runUsingGlfwHandle(Consumer action) { - if (!Display.isCreated()) { - MinecraftClientEvents.READY.register(mc -> { - if (isHandleAvailable()) { - action.accept(getWindowHandle()); - } - }); - } else if (isHandleAvailable()) { - action.accept(getWindowHandle()); - } - } -} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/util/WindowAccess.java b/1.8.9/src/main/java/io/github/axolotlclient/util/WindowAccess.java new file mode 100644 index 000000000..d0c1ca33e --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/util/WindowAccess.java @@ -0,0 +1,222 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import org.lwjgl.glfw.GLFW; +import org.lwjgl.sdl.SDLMouse; + +public sealed abstract class WindowAccess permits WindowAccess.GLFWAccess, WindowAccess.SDLAccess, WindowAccess.NoOpAccess { + + private static final boolean GLFW_AVAILABLE = checkGlfwAvailable(); + private static final boolean SDL_AVAILABLE = checkSDLAvailable(); + + public static boolean isGlfwAvailable() { + return WindowAccess.GLFW_AVAILABLE; + } + + public static boolean isSdlAvailable() { + return WindowAccess.SDL_AVAILABLE; + } + + public enum Cursor { + RESIZE_ALL, + ARROW, + RESIZE_NWSE, + RESIZE_NESW + } + + public abstract long createCursor(Cursor cursor); + + public abstract void setCursor(long cursor); + + public abstract void destroyCursors(long... cursors); + + public abstract boolean rawMouseMotionAvailable(); + + public abstract void setRawMouseMotion(boolean enabled); + + private static final WindowAccess INSTANCE = create(); + + public static WindowAccess getInstance() { + return INSTANCE; + } + + private static boolean checkGlfwAvailable() { + try { + Class.forName("org.lwjgl.glfw.GLFW"); + return true; + } catch (Throwable ignored) { + } + return false; + } + + private static boolean checkSDLAvailable() { + try { + Class.forName("org.lwjgl.sdl.SDL"); + return true; + } catch (Throwable ignored) { + } + return false; + } + + private static WindowAccess create() { + if (GLFW_AVAILABLE) { + return new GLFWAccess(); + } else if (SDL_AVAILABLE) { + return new SDLAccess(); + } + return new NoOpAccess(); + } + + final static class NoOpAccess extends WindowAccess { + + @Override + public long createCursor(Cursor cursor) { + return 0; + } + + @Override + public void setCursor(long cursor) { + + } + + @Override + public void destroyCursors(long... cursors) { + + } + + @Override + public boolean rawMouseMotionAvailable() { + return false; + } + + @Override + public void setRawMouseMotion(boolean enabled) { + + } + } + + final static class SDLAccess extends WindowAccess { + + @Override + public long createCursor(Cursor cursor) { + return SDLMouse.SDL_CreateSystemCursor(switch (cursor) { + case RESIZE_ALL -> SDLMouse.SDL_SYSTEM_CURSOR_MOVE; + case ARROW -> SDLMouse.SDL_SYSTEM_CURSOR_DEFAULT; + case RESIZE_NWSE -> SDLMouse.SDL_SYSTEM_CURSOR_NWSE_RESIZE; + case RESIZE_NESW -> SDLMouse.SDL_SYSTEM_CURSOR_NESW_RESIZE; + }); + } + + @Override + public void setCursor(long cursor) { + SDLMouse.SDL_SetCursor(cursor); + } + + @Override + public void destroyCursors(long... cursors) { + for (long c : cursors) { + SDLMouse.SDL_DestroyCursor(c); + } + } + + @Override + public boolean rawMouseMotionAvailable() { + return false; + } + + @Override + public void setRawMouseMotion(boolean enabled) { + + } + } + + final static class GLFWAccess extends WindowAccess { + + @Override + public long createCursor(Cursor cursor) { + return GLFW.glfwCreateStandardCursor(switch (cursor) { + case RESIZE_ALL -> GLFW.GLFW_RESIZE_ALL_CURSOR; + case ARROW -> GLFW.GLFW_ARROW_CURSOR; + case RESIZE_NWSE -> GLFW.GLFW_RESIZE_NWSE_CURSOR; + case RESIZE_NESW -> GLFW.GLFW_RESIZE_NESW_CURSOR; + }); + } + + @Override + public void setCursor(long cursor) { + GLFW.glfwSetCursor(WindowHandleAccess.getWindowHandle(), cursor); + } + + @Override + public void destroyCursors(long... cursors) { + for (long c : cursors) { + GLFW.glfwDestroyCursor(c); + } + } + + @Override + public boolean rawMouseMotionAvailable() { + return WindowHandleAccess.isHandleAvailable(); + } + + @Override + public void setRawMouseMotion(boolean enabled) { + GLFW.glfwSetInputMode(WindowHandleAccess.getWindowHandle(), GLFW.GLFW_RAW_MOUSE_MOTION, enabled ? 1 : 0); + } + } + + public static class WindowHandleAccess { + + private static MethodHandle getHandle; + + static { + try { + getHandle = MethodHandles.lookup().findStatic(Class.forName("org.lwjgl.opengl.Display"), "getHandle", MethodType.methodType(long.class)); + } catch (Throwable ignored) { + } + } + + private static long windowHandle = -1; + + public static long getWindowHandle() { + if (windowHandle == -1) { + try { + windowHandle = (long) getHandle.invoke(); + } catch (Throwable ignored) { + } + } + return windowHandle; + } + + // Since the reflection used for this only works with legacy-lwjgl3 it's possible for us not being able to access the window handle. + // This however should not lead to a crash despite compiling against glfw. + public static boolean isHandleAvailable() { + return getWindowHandle() != -1; + } + } +} diff --git a/1.8.9/src/main/resources/fabric.mod.json b/1.8.9/src/main/resources/fabric.mod.json index 6eaebc52a..d07fd43a4 100644 --- a/1.8.9/src/main/resources/fabric.mod.json +++ b/1.8.9/src/main/resources/fabric.mod.json @@ -122,7 +122,8 @@ "osl-keybinds": "*", "osl-entrypoints": "*", "axolotlclientconfig": "*", - "axolotlclient-common": "*" + "axolotlclient-common": "*", + "legacy-lwjgl3": ">1.2.8" } } } diff --git a/build.gradle.kts b/build.gradle.kts index 8c94b4349..742c79f98 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -54,6 +54,7 @@ allprojects { } mavenLocal() mavenCentral() + maven("https://central.sonatype.com/repository/maven-snapshots") } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 2404ebeba..78f7d782b 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { testRuntimeOnly(compileOnly("it.unimi.dsi:fastutil:8.2.1")!!) testRuntimeOnly(compileOnly("org.lwjgl:lwjgl-glfw:3.3.2")!!) testRuntimeOnly(compileOnly("org.lwjgl:lwjgl-tinyfd:3.2.2")!!) + testRuntimeOnly(compileOnly("org.lwjgl:lwjgl-sdl:3.4.0-SNAPSHOT")!!) shadow(implementation("io.github.CDAGaming:DiscordIPC:0.10.2") { isTransitive = false diff --git a/common/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotCopying.java b/common/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotCopying.java index 3ba5f40fc..bd7f1eb47 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotCopying.java +++ b/common/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotCopying.java @@ -29,41 +29,57 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import io.github.axolotlclient.AxolotlClientCommon; -import lombok.AllArgsConstructor; import lombok.experimental.UtilityClass; import org.jetbrains.annotations.NotNull; import org.lwjgl.glfw.GLFW; +import org.lwjgl.sdl.SDLClipboard; +import org.lwjgl.sdl.SDL_ClipboardCleanupCallback; +import org.lwjgl.sdl.SDL_ClipboardDataCallback; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; @UtilityClass public class ScreenshotCopying { static { - boolean wayland = false; + boolean sdl = false; try { - wayland = GLFW.glfwGetPlatform() == GLFW.GLFW_PLATFORM_WAYLAND; - } catch (Throwable ignored) { + Class.forName("org.lwjgl.sdl.SDL"); + sdl = true; + } catch (Throwable ignored) {} + SDL_AVAILABLE = sdl; + boolean wayland = false; + if (!sdl) { + try { + wayland = GLFW.glfwGetPlatform() == GLFW.GLFW_PLATFORM_WAYLAND; + } catch (Throwable ignored) { + } } IS_WAYLAND = wayland; } + private static final boolean SDL_AVAILABLE; private static final boolean IS_WAYLAND; public void copy(Path file) { - if (IS_WAYLAND) { - copyWayland(file); - } else { - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new FileTransferable(file.toFile()), null); + if (!SDL_AVAILABLE || !copySdl(file)) { + if (IS_WAYLAND) { + copyWayland(file); + } else { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new FileTransferable(file.toFile()), null); + } } } private void copyWayland(Path f) { try { - ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy -t image/png < '" + f.toAbsolutePath()+"'"); + ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy -t image/png < '" + f.toAbsolutePath() + "'"); Process p = builder.start(); p.waitFor(); } catch (IOException | InterruptedException ignored) { @@ -72,24 +88,50 @@ private void copyWayland(Path f) { } public void copy(byte[] image) { - if (IS_WAYLAND) { - try { - Path i = Files.createTempFile("axolotlclient_screenshot", ".png"); - Files.write(i, image); - copyWayland(i); - Files.delete(i); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().error("Failed to copy image using temporary file!"); + if (!SDL_AVAILABLE || !SDLFence.copySdl(image)) { + if (IS_WAYLAND) { + try { + Path i = Files.createTempFile("axolotlclient_screenshot", ".png"); + Files.write(i, image); + copyWayland(i); + Files.delete(i); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to copy image using temporary file!"); + } + } else { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new ImageTransferable(image), null); } - } else { - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new ImageTransferable(image), null); } } - @AllArgsConstructor - protected static class ImageTransferable implements Transferable { - private final byte[] image; + private boolean copySdl(Path p) { + try { + return SDLFence.copySdl(Files.readAllBytes(p)); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().error("Failed to read screenshot!", e); + } + return false; + } + + // Classloading fence... + static class SDLFence { + private static boolean copySdl(byte[] data) { + try (MemoryStack stack = MemoryStack.stackPush()) { + var mimetypesBuf = stack.UTF8("image/png"); + var mimetypes = stack.pointers(mimetypesBuf); + var dataBuf = ByteBuffer.allocateDirect(data.length).put(data).flip(); + var pointer = MemoryUtil.memAddress(dataBuf); + return SDLClipboard.SDL_SetClipboardData(SDL_ClipboardDataCallback.create((userdata, mime_type, size) -> { + var sizeBuf = MemoryUtil.memLongBuffer(size, 8); + sizeBuf.put(0, data.length); + return userdata; + }), + SDL_ClipboardCleanupCallback.create(userdata -> MemoryUtil.memFree(dataBuf)), pointer, mimetypes); + } + } + } + protected record ImageTransferable(byte[] image) implements Transferable { @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[]{DataFlavor.imageFlavor}; @@ -107,10 +149,7 @@ public Object getTransferData(DataFlavor flavor) throws IOException { } } - @AllArgsConstructor - protected static class FileTransferable implements Transferable { - private final File file; - + protected record FileTransferable(File file) implements Transferable { @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[]{DataFlavor.javaFileListFlavor}; @@ -122,7 +161,7 @@ public boolean isDataFlavorSupported(DataFlavor flavor) { } @Override - public Object getTransferData(DataFlavor flavor) { + public @NotNull Object getTransferData(DataFlavor flavor) { final ArrayList files = new ArrayList<>(); files.add(file); return files; diff --git a/gradle.properties b/gradle.properties index a6fda7db5..35215e96c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,6 +30,6 @@ fapi_121=0.116.6 fabric_cts8=0.42.0+1.16 osl=0.16.3 -legacy_lwgjl3=1.2.5+1.8.9 +legacy_lwgjl3=1.2.9+1.8.9 config=3.0.19 From a1c561c5eb244d6c8c6b2bc2f3b02b5f2b8bbaa1 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 7 Sep 2025 15:10:35 +0200 Subject: [PATCH 16/23] process legacy (64x32) skins --- .../skins/PlayerSkinTextureAccessor.java | 37 +++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 18 +++++++-- .../main/resources/axolotlclient.mixins.json | 1 + .../skins/PlayerSkinTextureAccessor.java | 37 +++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 17 +++++++-- .../main/resources/axolotlclient.mixins.json | 1 + .../skins/SkinTextureDownloaderAccessor.java | 36 ++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 17 +++++++-- .../main/resources/axolotlclient.mixins.json | 1 + .../skins/PlayerSkinTextureAccessor.java | 37 +++++++++++++++++++ .../modules/auth/skin/SkinManager.java | 17 +++++++-- .../main/resources/axolotlclient.mixins.json | 1 + .../modules/auth/skin/SkinManager.java | 16 ++++++-- .../modules/auth/skin/Asset.java | 1 - .../axolotlclient/modules/auth/skin/Skin.java | 7 +--- .../io/github/axolotlclient/util/Watcher.java | 2 +- 16 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java create mode 100644 1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java create mode 100644 1.21.7/src/main/java/io/github/axolotlclient/mixin/skins/SkinTextureDownloaderAccessor.java create mode 100644 1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java new file mode 100644 index 000000000..c14dc1a3d --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin.skins; + +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.PlayerSkinTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(PlayerSkinTexture.class) +public interface PlayerSkinTextureAccessor { + + @Invoker("remapTexture") + static NativeImage invokeRemapTexture(NativeImage img) { + throw new UnsupportedOperationException(); + } +} diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index c29158513..5de65dd8f 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,6 +37,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.skins.PlayerSkinTextureAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.MinecraftClient; @@ -62,18 +63,27 @@ public Skin read(Path p) { byteBuffer.put(in); byteBuffer.rewind(); try (var img = NativeImage.read(byteBuffer)) { - if (img.getWidth() != 64 || img.getHeight() != 64) return null; - slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + int width = img.getWidth(); + int height = img.getHeight(); + if (width != 64) return null; + if (height == 32) { + var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); + img2.writeFile(p); + slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + } else if (height != 64) { + return null; + } else { + slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + } } } - return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + return new Skin.Local(!slim, p, sha256); } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } return null; } - public CompletableFuture loadSkin(Skin skin) { var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); if (loadedTextures.contains(rl)) { diff --git a/1.16_combat-6/src/main/resources/axolotlclient.mixins.json b/1.16_combat-6/src/main/resources/axolotlclient.mixins.json index f9da4cbef..26c026cd2 100644 --- a/1.16_combat-6/src/main/resources/axolotlclient.mixins.json +++ b/1.16_combat-6/src/main/resources/axolotlclient.mixins.json @@ -62,6 +62,7 @@ "WorldRendererAccessor", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", + "skins.PlayerSkinTextureAccessor", "translation.LanguageMixin" ], "injectors": { diff --git a/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java b/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java new file mode 100644 index 000000000..be7933c05 --- /dev/null +++ b/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin.skins; + +import com.mojang.blaze3d.texture.NativeImage; +import net.minecraft.client.texture.PlayerSkinTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(PlayerSkinTexture.class) +public interface PlayerSkinTextureAccessor { + + @Invoker("remapTexture") + static NativeImage invokeRemapTexture(NativeImage img) { + throw new UnsupportedOperationException(); + } +} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 2e9daa0d6..d6df91a77 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,6 +37,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.skins.PlayerSkinTextureAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.MinecraftClient; @@ -55,10 +56,20 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { - if (img.getWidth() != 64 || img.getHeight() != 64) return null; - slim = (ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0); + int width = img.getWidth(); + int height = img.getHeight(); + if (width != 64) return null; + if (height == 32) { + var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); + img2.writeFile(p); + slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + } else if (height != 64) { + return null; + } else { + slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + } } - return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + return new Skin.Local(!slim, p, sha256); } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } diff --git a/1.20/src/main/resources/axolotlclient.mixins.json b/1.20/src/main/resources/axolotlclient.mixins.json index b8e2bce7c..89f22e672 100644 --- a/1.20/src/main/resources/axolotlclient.mixins.json +++ b/1.20/src/main/resources/axolotlclient.mixins.json @@ -61,6 +61,7 @@ "WorldRendererAccessor", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", + "skins.PlayerSkinTextureAccessor", "translation.TranslationStorageMixin" ], "injectors": { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/mixin/skins/SkinTextureDownloaderAccessor.java b/1.21.7/src/main/java/io/github/axolotlclient/mixin/skins/SkinTextureDownloaderAccessor.java new file mode 100644 index 000000000..b561887b4 --- /dev/null +++ b/1.21.7/src/main/java/io/github/axolotlclient/mixin/skins/SkinTextureDownloaderAccessor.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin.skins; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.renderer.texture.SkinTextureDownloader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(SkinTextureDownloader.class) +public interface SkinTextureDownloaderAccessor { + @Invoker("processLegacySkin") + static NativeImage invokeProcessLegacySkin(NativeImage img, String url) { + throw new UnsupportedOperationException(); + } +} diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 6b5d51131..e6362ed6d 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,6 +37,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.skins.SkinTextureDownloaderAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.Minecraft; @@ -55,10 +56,20 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { - if (img.getWidth() != 64 || img.getHeight() != 64) return null; - slim = ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0; + int width = img.getWidth(); + int height = img.getHeight(); + if (width != 64) return null; + if (height == 32) { + var img2 = SkinTextureDownloaderAccessor.invokeProcessLegacySkin(img, "local"); + img2.writeToFile(p); + slim = ClientColors.ARGB.alpha(img2.getPixel(47, 63)) == 0; + } else if (height != 64) { + return null; + } else { + slim = ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0; + } } - return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + return new Skin.Local(!slim, p, sha256); } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } diff --git a/1.21.7/src/main/resources/axolotlclient.mixins.json b/1.21.7/src/main/resources/axolotlclient.mixins.json index bf43bb69a..64f9e1196 100644 --- a/1.21.7/src/main/resources/axolotlclient.mixins.json +++ b/1.21.7/src/main/resources/axolotlclient.mixins.json @@ -65,6 +65,7 @@ "WorldListWidgetEntryMixin", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", + "skins.SkinTextureDownloaderAccessor", "translation.TranslationStorageMixin" ], "injectors": { diff --git a/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java b/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java new file mode 100644 index 000000000..be7933c05 --- /dev/null +++ b/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin.skins; + +import com.mojang.blaze3d.texture.NativeImage; +import net.minecraft.client.texture.PlayerSkinTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(PlayerSkinTexture.class) +public interface PlayerSkinTextureAccessor { + + @Invoker("remapTexture") + static NativeImage invokeRemapTexture(NativeImage img) { + throw new UnsupportedOperationException(); + } +} diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index eca750a52..dfa8d6c22 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,6 +37,7 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; +import io.github.axolotlclient.mixin.skins.PlayerSkinTextureAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.MinecraftClient; @@ -55,10 +56,20 @@ public Skin read(Path p) { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); try (var img = NativeImage.read(in)) { - if (img.getWidth() != 64 || img.getHeight() != 64) return null; - slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + int width = img.getWidth(); + int height = img.getHeight(); + if (width != 64) return null; + if (height == 32) { + var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); + img2.writeFile(p); + slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + } else if (height != 64) { + return null; + } else { + slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + } } - return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + return new Skin.Local(!slim, p, sha256); } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } diff --git a/1.21/src/main/resources/axolotlclient.mixins.json b/1.21/src/main/resources/axolotlclient.mixins.json index 2491658dd..dec975510 100644 --- a/1.21/src/main/resources/axolotlclient.mixins.json +++ b/1.21/src/main/resources/axolotlclient.mixins.json @@ -60,6 +60,7 @@ "WorldRendererAccessor", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", + "skins.PlayerSkinTextureAccessor", "translation.TranslationStorageMixin" ], "injectors": { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 16e547bdf..9477317b6 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -42,6 +42,7 @@ import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.Minecraft; import net.minecraft.client.render.texture.DynamicTexture; +import net.minecraft.client.render.texture.SkinImageProcessor; import net.minecraft.client.resource.skin.DefaultSkinUtils; import net.minecraft.resource.Identifier; @@ -58,17 +59,24 @@ public Skin read(Path p) { sha256 = Hashing.sha256().hashBytes(in).toString(); try (var bs = new ByteArrayInputStream(in)) { var img = ImageIO.read(bs); - if (img.getWidth() != 64 || img.getHeight() != 64) return null; - slim = (ClientColors.ARGB.alpha(img.getRGB(47, 63)) == 0); + int height = img.getHeight(); + int width = img.getWidth(); + if (width != 64) return null; + if (height == 32) { + img = new SkinImageProcessor().process(img); + try (var out = Files.newOutputStream(p)) { + ImageIO.write(img, "png", out); + } + } else if (height != 64) return null; + slim = ClientColors.ARGB.alpha(img.getRGB(47, 63)) == 0; } - return new Skin.Local(!slim, Hashing.sha512().hashUnencodedChars(p.toString()).toString(), p, sha256); + return new Skin.Local(!slim, p, sha256); } catch (Exception e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); } return null; } - public CompletableFuture loadSkin(Skin skin) { var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); if (loadedTextures.contains(rl)) { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java index 8a66a30de..d51be8f04 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Asset.java @@ -29,7 +29,6 @@ import io.github.axolotlclient.modules.auth.MSApi; public interface Asset { - String id(); default boolean isOnline() { return false; diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index 6be110bea..fdba14c7d 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -34,7 +34,7 @@ public interface Skin extends Asset { boolean isClassicVariant(); - record Local(boolean classic, String id, Path file, String textureKey) implements Skin { + record Local(boolean classic, Path file, String textureKey) implements Skin { @Override public boolean isClassicVariant() { @@ -75,11 +75,6 @@ public boolean isClassicVariant() { return online.isClassicVariant(); } - @Override - public String id() { - return online.id(); - } - @Override public CompletableFuture image() { return local.image(); diff --git a/common/src/main/java/io/github/axolotlclient/util/Watcher.java b/common/src/main/java/io/github/axolotlclient/util/Watcher.java index e01b81bbd..4993aed0f 100644 --- a/common/src/main/java/io/github/axolotlclient/util/Watcher.java +++ b/common/src/main/java/io/github/axolotlclient/util/Watcher.java @@ -98,7 +98,7 @@ public boolean pollForChanges() throws IOException { while ((watchKey = this.watcher.poll()) != null) { for (WatchEvent watchEvent : watchKey.pollEvents()) { bl = true; - if (watchKey.watchable() == this.path && watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) { + if (watchKey.watchable() == this.path && watchEvent.context() != null) { Path path = this.path.resolve((Path) watchEvent.context()); if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { this.watchDir(path); From 6288e5dbb7f795cbaeafed45da6d4d5440f1b3b2 Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 7 Sep 2025 15:19:46 +0200 Subject: [PATCH 17/23] fix a bug --- .../java/io/github/axolotlclient/modules/hypixel/AutoGG.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/io/github/axolotlclient/modules/hypixel/AutoGG.java b/common/src/main/java/io/github/axolotlclient/modules/hypixel/AutoGG.java index b6bb7200d..aff67d7ac 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/hypixel/AutoGG.java +++ b/common/src/main/java/io/github/axolotlclient/modules/hypixel/AutoGG.java @@ -159,7 +159,7 @@ private List addToList(String... strings) { private void onMessage(ReceiveChatMessageEvent event) { String message = event.getOriginalMessage(); - if (client.br$isLocalServer()) { + if (!client.br$isLocalServer()) { serverMap.keySet().forEach(s -> { if (serverMap.get(s).get() && client.br$getServerAddress().contains(s)) { if (gf.get()) { From 3c518f4090e2288a1444d4f2a9f1e2fcd8ca895b Mon Sep 17 00:00:00 2001 From: moehreag Date: Sun, 7 Sep 2025 19:13:29 +0200 Subject: [PATCH 18/23] add buttons to switch skin variants --- .../auth/skin/SkinManagementScreen.java | 111 ++++++++------ .../modules/auth/skin/SkinManager.java | 17 ++- .../modules/auth/skin/SkinWidget.java | 2 +- .../auth/skin/SkinManagementScreen.java | 118 +++++++++------ .../modules/auth/skin/SkinManager.java | 15 +- .../modules/auth/skin/SkinWidget.java | 2 +- .../auth/skin/SkinManagementScreen.java | 126 +++++++++++----- .../modules/auth/skin/SkinManager.java | 15 +- .../modules/auth/skin/SkinWidget.java | 2 +- .../auth/skin/SkinManagementScreen.java | 126 +++++++++++----- .../modules/auth/skin/SkinManager.java | 15 +- .../modules/auth/skin/SkinWidget.java | 2 +- .../auth/skin/SkinManagementScreen.java | 139 ++++++++++-------- .../modules/auth/skin/SkinManager.java | 21 ++- .../modules/auth/skin/SkinRenderer.java | 4 +- .../modules/auth/skin/SkinWidget.java | 2 +- .../axolotlclient/modules/auth/MSApi.java | 72 ++++++++- .../axolotlclient/modules/auth/skin/Skin.java | 48 +++++- .../assets/axolotlclient/lang/en_us.json | 6 +- .../textures/gui/sprites/slim.png | Bin 0 -> 215 bytes .../textures/gui/sprites/wide.png | Bin 0 -> 217 bytes 21 files changed, 586 insertions(+), 257 deletions(-) create mode 100644 common/src/main/resources/assets/axolotlclient/textures/gui/sprites/slim.png create mode 100644 common/src/main/resources/assets/axolotlclient/textures/gui/sprites/wide.png diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index c97f4b47d..b646afc54 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -41,7 +41,9 @@ import io.github.axolotlclient.modules.hud.util.DrawUtil; import io.github.axolotlclient.util.ButtonWidgetTextures; import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; +import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; @@ -56,6 +58,7 @@ import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.client.render.Tessellator; import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.resource.language.I18n; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; import net.minecraft.text.MutableText; @@ -87,7 +90,10 @@ public SkinManagementScreen(Screen parent, Account account) { super(new TranslatableText("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); + loadSkinsList(); + }); if (account.needsRefresh()) { refreshFuture = account.refresh(Auth.getInstance().getMsApi()); } else { @@ -367,14 +373,33 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; - packs.forEach(p -> { - try { - Files.copy(p, SKINS_DIR.resolve(p.getFileName())); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); - } - }); - loadSkinsList(); + + CompletableFuture[] futs = new CompletableFuture[packs.size()]; + for (int i = 0; i < packs.size(); i++) { + Path p = packs.get(i); + futs[i] = CompletableFuture.runAsync(() -> { + try { + var target = SKINS_DIR.resolve(p.getFileName()); + if (Files.exists(target)) { + int counter = 0; + do { + counter++; + target = target.resolveSibling(target.getFileName().toString()+"_"+counter); + } while (Files.exists(target)); + } + var skin = Auth.getInstance().getSkinManager().read(p, false); + if (skin != null) { + Files.write(target, skin.image().join()); + } else { + AxolotlClientCommon.getInstance().getLogger().info("Skipping dragged file {} because it does not seem to be a valid skin!", p); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.not_copied", p.getFileName()); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }, ThreadExecuter.service()); + } + CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { @@ -538,9 +563,40 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { super(0, 0, widget.getWidth(), height, LiteralText.EMPTY); widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); + class SpriteButton extends ButtonWidget { + private Identifier sprite; + + public SpriteButton(Text message, PressAction onPress, Identifier sprite) { + super(0, 0, 11, 11, message, onPress); + this.sprite = sprite; + } + + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); + client.getTextureManager().bindTexture(sprite); + drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); + if (this.isHovered()) { + tooltip = getMessage(); + } + } + } + if (asset instanceof Skin skin) { + var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); + var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); + var slimText = new TranslatableText("skins.manage.variant.classic"); + var wideText = new TranslatableText("skins.manage.variant.slim"); + actionButtons.add(new SpriteButton(skin.classicVariant() ? wideText : slimText, btn -> { + var self = (SpriteButton) btn; + skin.classicVariant(!skin.classicVariant()); + self.sprite = skin.classicVariant() ? slimSprite : wideSprite; + self.setMessage(skin.classicVariant() ? wideText : slimText); + }, skin.classicVariant() ? slimSprite : wideSprite)); + } if (asset != null) { if (asset.isLocal()) { - var delete = new ButtonWidget(0, 0, 11, 11, new TranslatableText("skins.manage.delete"), btn -> { + this.actionButtons.add(new SpriteButton(new TranslatableText("skins.manage.delete"), btn -> { btn.active = false; client.openScreen(new ConfirmScreen(confirmed -> { client.openScreen(SkinManagementScreen.this); @@ -557,25 +613,10 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { new TranslatableText("skins.manage.delete.confirm.desc_active") : new TranslatableText("skins.manage.delete.confirm.desc") ).br$color(Colors.RED.toInt()))); - }) { - - private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); - - @Override - public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); - DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); - client.getTextureManager().bindTexture(sprite); - drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - if (this.isHovered()) { - tooltip = getMessage(); - } - } - }; - this.actionButtons.add(delete); + }, new Identifier("axolotlclient", "textures/gui/sprites/delete.png"))); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = new ButtonWidget(0, 0, 11, 11, new TranslatableText("skins.manage.download"), btn -> { + this.actionButtons.add(new SpriteButton(new TranslatableText("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -588,21 +629,7 @@ public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float del refreshCurrentList(); btn.active = true; }); - }) { - private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); - DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); - client.getTextureManager().bindTexture(sprite); - drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - if (this.isHovered()) { - tooltip = getMessage(); - } - } - }; - this.actionButtons.add(download); + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png"))); } } if (label != null) { diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 5de65dd8f..80e1d96bc 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -51,8 +51,12 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); - @SuppressWarnings("UnstableApiUsage") public Skin read(Path p) { + return read(p, true); + } + + @SuppressWarnings("UnstableApiUsage") + public Skin read(Path p, boolean fix) { boolean slim; String sha256; try { @@ -67,13 +71,16 @@ public Skin read(Path p) { int height = img.getHeight(); if (width != 64) return null; if (height == 32) { - var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); - img2.writeFile(p); - slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + if (fix) { + try (var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img)) { + img2.writeFile(p); + } + } + slim = false; } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; } } } diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 2efd72a66..10a0e265c 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -90,7 +90,7 @@ public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); - classic = skin.isClassicVariant(); + classic = skin.classicVariant(); } else { var uuid = UUIDHelper.fromUndashed(owner.getUuid()); classic = DefaultSkinHelper.getModel(uuid).equals("default"); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index da3d50707..0d984229d 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -39,7 +39,9 @@ import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; +import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; @@ -78,7 +80,10 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); + loadSkinsList(); + }); if (account.needsRefresh()) { refreshFuture = account.refresh(Auth.getInstance().getMsApi()); } else { @@ -333,14 +338,33 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; - packs.forEach(p -> { - try { - Files.copy(p, SKINS_DIR.resolve(p.getFileName())); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); - } - }); - loadSkinsList(); + + CompletableFuture[] futs = new CompletableFuture[packs.size()]; + for (int i = 0; i < packs.size(); i++) { + Path p = packs.get(i); + futs[i] = CompletableFuture.runAsync(() -> { + try { + var target = SKINS_DIR.resolve(p.getFileName()); + if (Files.exists(target)) { + int counter = 0; + do { + counter++; + target = target.resolveSibling(target.getFileName().toString()+"_"+counter); + } while (Files.exists(target)); + } + var skin = Auth.getInstance().getSkinManager().read(p, false); + if (skin != null) { + Files.write(target, skin.image().join()); + } else { + AxolotlClientCommon.getInstance().getLogger().info("Skipping dragged file {} because it does not seem to be a valid skin!", p); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.not_copied", p.getFileName()); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }, ThreadExecuter.service()); + } + CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { @@ -511,8 +535,46 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { + class SpriteButton extends ButtonWidget { + private Identifier sprite; + + public SpriteButton(Text message, PressAction onPress, Identifier sprite) { + super(0, 0, 11, 11, message, onPress, DEFAULT_NARRATION); + this.sprite = sprite; + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + public void setMessage(Text message) { + super.setMessage(message); + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawTexture(sprite, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } + } + if (asset instanceof Skin skin) { + var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); + var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); + var slimText = Text.translatable("skins.manage.variant.classic"); + var wideText = Text.translatable("skins.manage.variant.slim"); + actionButtons.add(new SpriteButton(skin.classicVariant() ? wideText : slimText, btn -> { + var self = (SpriteButton) btn; + skin.classicVariant(!skin.classicVariant()); + self.sprite = skin.classicVariant() ? slimSprite : wideSprite; + self.setMessage(skin.classicVariant() ? wideText : slimText); + }, skin.classicVariant() ? slimSprite : wideSprite)); + } if (asset.isLocal()) { - var delete = new ButtonWidget(0, 0, 11, 11, Text.translatable("skins.manage.delete"), btn -> { + this.actionButtons.add(new SpriteButton(Text.translatable("skins.manage.delete"), btn -> { btn.active = false; client.setScreen(new ConfirmScreen(confirmed -> { client.setScreen(SkinManagementScreen.this); @@ -529,25 +591,10 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { Text.translatable("skins.manage.delete.confirm.desc_active") : Text.translatable("skins.manage.delete.confirm.desc") ).br$color(Colors.RED.toInt()))); - }, Supplier::get) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); - - @Override - protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { - - } - }; - delete.setTooltip(Tooltip.create(delete.getMessage())); - this.actionButtons.add(delete); + }, new Identifier("axolotlclient", "textures/gui/sprites/delete.png"))); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = new ButtonWidget(0, 0, 11, 11, Text.translatable("skins.manage.download"), btn -> { + this.actionButtons.add(new SpriteButton(Text.translatable("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -560,22 +607,7 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int refreshCurrentList(); btn.active = true; }); - }, Supplier::get) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { - - } - }; - download.setTooltip(Tooltip.create(download.getMessage())); - this.actionButtons.add(download); + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png"))); } } if (label != null) { diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index d6df91a77..a76162d07 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -50,6 +50,10 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); public Skin read(Path p) { + return read(p, true); + } + + public Skin read(Path p, boolean fix) { boolean slim; String sha256; try { @@ -60,13 +64,16 @@ public Skin read(Path p) { int height = img.getHeight(); if (width != 64) return null; if (height == 32) { - var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); - img2.writeFile(p); - slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + if (fix) { + try (var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img)) { + img2.writeFile(p); + } + } + slim = false; } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; } } return new Skin.Local(!slim, p, sha256); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 79acc96bc..17f8982a7 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -92,7 +92,7 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); - classic = skin.isClassicVariant(); + classic = skin.classicVariant(); } else { var uuid = UUIDHelper.fromUndashed(owner.getUuid()); classic = DefaultSkinHelper.getModel(uuid).equals("default"); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 2401f76a9..a05b9a2af 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -42,11 +42,14 @@ import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; +import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.*; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -85,7 +88,10 @@ public SkinManagementScreen(Screen parent, Account account) { super(Component.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); + loadSkinsList(); + }); if (account.needsRefresh()) { refreshFuture = account.refresh(Auth.getInstance().getMsApi()); } else { @@ -150,7 +156,7 @@ protected void init() { SkinImportUtil.openImportSkinDialog().thenAccept(this::onFilesDrop).thenRun(() -> btn.active = true); }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); - importButton.setPosition(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13); + importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -310,14 +316,33 @@ private void populateSkinList(List skins, int columns) { @Override public void onFilesDrop(List packs) { if (packs.isEmpty()) return; - packs.forEach(p -> { - try { - Files.copy(p, SKINS_DIR.resolve(p.getFileName())); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); - } - }); - loadSkinsList(); + + CompletableFuture[] futs = new CompletableFuture[packs.size()]; + for (int i = 0; i < packs.size(); i++) { + Path p = packs.get(i); + futs[i] = CompletableFuture.runAsync(() -> { + try { + var target = SKINS_DIR.resolve(p.getFileName()); + if (Files.exists(target)) { + int counter = 0; + do { + counter++; + target = target.resolveSibling(target.getFileName().toString() + "_" + counter); + } while (Files.exists(target)); + } + var skin = Auth.getInstance().getSkinManager().read(p, false); + if (skin != null) { + Files.write(target, skin.image().join()); + } else { + AxolotlClientCommon.getInstance().getLogger().info("Skipping dragged file {} because it does not seem to be a valid skin!", p); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.not_copied", p.getFileName()); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }, ThreadExecuter.service()); + } + CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { @@ -467,31 +492,66 @@ public Entry(int height, SkinWidget widget, @Nullable Component label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { + class SpriteButton extends Button { + private ResourceLocation sprite; + + public SpriteButton(Component message, OnPress onPress, ResourceLocation sprite) { + super(0, 0, 11, 11, message, onPress, DEFAULT_NARRATION); + this.sprite = sprite; + setTooltip(Tooltip.create(message, Component.empty())); + } + + @Override + public void setMessage(Component message) { + super.setMessage(message); + setTooltip(Tooltip.create(message, Component.empty())); + } + + @Override + protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.renderWidget(graphics, mouseX, mouseY, delta); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, this.getX() + 2, this.getY() + 2, 7, 7); + } + + @Override + public void renderString(GuiGraphics guiGraphics, Font font, int color) { + + } + } + if (asset instanceof Skin skin) { + var wideSprite = ResourceLocation.fromNamespaceAndPath("axolotlclient", "wide"); + var slimSprite = ResourceLocation.fromNamespaceAndPath("axolotlclient", "slim"); + var slimText = Component.translatable("skins.manage.variant.classic"); + var wideText = Component.translatable("skins.manage.variant.slim"); + actionButtons.add(new SpriteButton(skin.classicVariant() ? wideText : slimText, btn -> { + var self = (SpriteButton) btn; + skin.classicVariant(!skin.classicVariant()); + self.sprite = skin.classicVariant() ? slimSprite : wideSprite; + self.setMessage(skin.classicVariant() ? wideText : slimText); + }, skin.classicVariant() ? slimSprite : wideSprite)); + } if (asset.isLocal()) { - var delete = SpriteIconButton.builder(Component.translatable("skins.manage.delete"), btn -> { - btn.active = false; - minecraft.setScreen(new ConfirmScreen(confirmed -> { - minecraft.setScreen(SkinManagementScreen.this); - if (confirmed) { - try { - Files.delete(asset.file()); - refreshCurrentList(); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); - } + this.actionButtons.add(new SpriteButton(Component.translatable("skins.manage.delete"), btn -> { + btn.active = false; + minecraft.setScreen(new ConfirmScreen(confirmed -> { + minecraft.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); } - btn.active = true; - }, Component.translatable("skins.manage.delete.confirm"), (asset.active() ? - Component.translatable("skins.manage.delete.confirm.desc_active") : - Component.translatable("skins.manage.delete.confirm.desc")) - .withColor(Colors.RED.toInt()))); - }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"), 7, 7).size(11, 11) - .build(); - delete.setTooltip(Tooltip.create(delete.getMessage())); - this.actionButtons.add(delete); + } + btn.active = true; + }, Component.translatable("skins.manage.delete.confirm"), (asset.active() ? + Component.translatable("skins.manage.delete.confirm.desc_active") : + Component.translatable("skins.manage.delete.confirm.desc") + ).withColor(Colors.RED.toInt()))); + }, ResourceLocation.fromNamespaceAndPath("axolotlclient", "delete"))); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = SpriteIconButton.builder(Component.translatable("skins.manage.download"), btn -> { + this.actionButtons.add(new SpriteButton(Component.translatable("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -504,9 +564,7 @@ public Entry(int height, SkinWidget widget, @Nullable Component label) { refreshCurrentList(); btn.active = true; }); - }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); - download.setTooltip(Tooltip.create(download.getMessage())); - this.actionButtons.add(download); + }, ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"))); } } if (label != null) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index e6362ed6d..891d884e5 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -50,6 +50,10 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); public Skin read(Path p) { + return read(p, true); + } + + public Skin read(Path p, boolean fix) { boolean slim; String sha256; try { @@ -60,13 +64,16 @@ public Skin read(Path p) { int height = img.getHeight(); if (width != 64) return null; if (height == 32) { - var img2 = SkinTextureDownloaderAccessor.invokeProcessLegacySkin(img, "local"); - img2.writeToFile(p); - slim = ClientColors.ARGB.alpha(img2.getPixel(47, 63)) == 0; + if (fix) { + try (var img2 = SkinTextureDownloaderAccessor.invokeProcessLegacySkin(img, "local")) { + img2.writeToFile(p); + } + } + slim = false; } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixel(47, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixel(63, 63)) == 0; } } return new Skin.Local(!slim, p, sha256); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index f9a186cc8..1ec8f110d 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -94,7 +94,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); - classic = skin.isClassicVariant(); + classic = skin.classicVariant(); } else { var skin = DefaultPlayerSkin.get(UUIDHelper.fromUndashed(owner.getUuid())); classic = skin.model() == PlayerSkin.Model.WIDE; diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index b1f206548..34c75aa30 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -38,9 +38,12 @@ import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; +import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.Element; import net.minecraft.client.gui.ElementPath; import net.minecraft.client.gui.GuiGraphics; @@ -85,7 +88,10 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); + loadSkinsList(); + }); if (account.needsRefresh()) { refreshFuture = account.refresh(Auth.getInstance().getMsApi()); } else { @@ -149,7 +155,7 @@ protected void init() { SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); - importButton.setPosition(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13); + importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -308,14 +314,33 @@ private void populateSkinList(List skins, int columns) { @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; - packs.forEach(p -> { - try { - Files.copy(p, SKINS_DIR.resolve(p.getFileName())); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); - } - }); - loadSkinsList(); + + CompletableFuture[] futs = new CompletableFuture[packs.size()]; + for (int i = 0; i < packs.size(); i++) { + Path p = packs.get(i); + futs[i] = CompletableFuture.runAsync(() -> { + try { + var target = SKINS_DIR.resolve(p.getFileName()); + if (Files.exists(target)) { + int counter = 0; + do { + counter++; + target = target.resolveSibling(target.getFileName().toString() + "_" + counter); + } while (Files.exists(target)); + } + var skin = Auth.getInstance().getSkinManager().read(p, false); + if (skin != null) { + Files.write(target, skin.image().join()); + } else { + AxolotlClientCommon.getInstance().getLogger().info("Skipping dragged file {} because it does not seem to be a valid skin!", p); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.not_copied", p.getFileName()); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }, ThreadExecuter.service()); + } + CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { @@ -474,31 +499,66 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { + class SpriteButton extends ButtonWidget { + private Identifier sprite; + + public SpriteButton(Text message, PressAction onPress, Identifier sprite) { + super(0, 0, 11, 11, message, onPress, DEFAULT_NARRATION); + this.sprite = sprite; + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + public void setMessage(Text message) { + super.setMessage(message); + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawGuiTexture(sprite, this.getX() + 2, this.getY() + 2, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } + } + if (asset instanceof Skin skin) { + var wideSprite = Identifier.of("axolotlclient", "wide"); + var slimSprite = Identifier.of("axolotlclient", "slim"); + var slimText = Text.translatable("skins.manage.variant.classic"); + var wideText = Text.translatable("skins.manage.variant.slim"); + actionButtons.add(new SpriteButton(skin.classicVariant() ? wideText : slimText, btn -> { + var self = (SpriteButton) btn; + skin.classicVariant(!skin.classicVariant()); + self.sprite = skin.classicVariant() ? slimSprite : wideSprite; + self.setMessage(skin.classicVariant() ? wideText : slimText); + }, skin.classicVariant() ? slimSprite : wideSprite)); + } if (asset.isLocal()) { - var delete = SpriteButtonWidget.builder(Text.translatable("skins.manage.delete"), btn -> { - btn.active = false; - client.setScreen(new ConfirmScreen(confirmed -> { - client.setScreen(SkinManagementScreen.this); - if (confirmed) { - try { - Files.delete(asset.file()); - refreshCurrentList(); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); - } + this.actionButtons.add(new SpriteButton(Text.translatable("skins.manage.delete"), btn -> { + btn.active = false; + client.setScreen(new ConfirmScreen(confirmed -> { + client.setScreen(SkinManagementScreen.this); + if (confirmed) { + try { + Files.delete(asset.file()); + refreshCurrentList(); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); } - btn.active = true; - }, Text.translatable("skins.manage.delete.confirm"), (asset.active() ? - Text.translatable("skins.manage.delete.confirm.desc_active") : - Text.translatable("skins.manage.delete.confirm.desc")) - .setColor(Colors.RED.toInt()))); - }, true).sprite(Identifier.of("axolotlclient", "delete"), 7, 7).dimensions(11, 11) - .build(); - delete.setTooltip(Tooltip.create(delete.getMessage())); - this.actionButtons.add(delete); + } + btn.active = true; + }, Text.translatable("skins.manage.delete.confirm"), (Text) (asset.active() ? + Text.translatable("skins.manage.delete.confirm.desc_active") : + Text.translatable("skins.manage.delete.confirm.desc") + ).br$color(Colors.RED.toInt()))); + }, Identifier.of("axolotlclient", "delete"))); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = SpriteButtonWidget.builder(Text.translatable("skins.manage.download"), btn -> { + this.actionButtons.add(new SpriteButton(Text.translatable("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -511,9 +571,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { refreshCurrentList(); btn.active = true; }); - }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); - download.setTooltip(Tooltip.create(download.getMessage())); - this.actionButtons.add(download); + }, Identifier.of("axolotlclient", "download"))); } } if (label != null) { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index dfa8d6c22..b59112311 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -50,6 +50,10 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); public Skin read(Path p) { + return read(p, true); + } + + public Skin read(Path p, boolean fix) { boolean slim; String sha256; try { @@ -60,13 +64,16 @@ public Skin read(Path p) { int height = img.getHeight(); if (width != 64) return null; if (height == 32) { - var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img); - img2.writeFile(p); - slim = ClientColors.ARGB.alpha(img2.getPixelColor(47, 63)) == 0; + if (fix) { + try (var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img)) { + img2.writeFile(p); + } + } + slim = false; } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(47, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; } } return new Skin.Local(!slim, p, sha256); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 55f94405e..d924bf4b2 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -93,7 +93,7 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); - classic = skin.isClassicVariant(); + classic = skin.classicVariant(); } else { var skin = DefaultSkinHelper.getSkin(UUIDHelper.fromUndashed(owner.getUuid())); classic = skin.model() == PlayerSkin.Model.WIDE; diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index d9e3839fa..a4c5f0e3e 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -39,6 +39,7 @@ import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Color; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ButtonWidget; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ClickableWidget; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.Element; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ParentElement; @@ -51,7 +52,9 @@ import io.github.axolotlclient.modules.hud.util.DrawUtil; import io.github.axolotlclient.util.ButtonWidgetTextures; import io.github.axolotlclient.util.ClientColors; +import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; +import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screen.ConfirmScreen; @@ -83,7 +86,10 @@ public SkinManagementScreen(Screen parent, Account account) { super(I18n.translate("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, this::loadSkinsList); + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); + loadSkinsList(); + }); if (account.needsRefresh()) { refreshFuture = account.refresh(Auth.getInstance().getMsApi()); } else { @@ -362,14 +368,33 @@ private void populateSkinList(List skins, int columns) { @Override public void onFileDrop(List packs) { if (packs.isEmpty()) return; - packs.forEach(p -> { - try { - Files.copy(p, SKINS_DIR.resolve(p.getFileName())); - } catch (IOException e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); - } - }); - loadSkinsList(); + + CompletableFuture[] futs = new CompletableFuture[packs.size()]; + for (int i = 0; i < packs.size(); i++) { + Path p = packs.get(i); + futs[i] = CompletableFuture.runAsync(() -> { + try { + var target = SKINS_DIR.resolve(p.getFileName()); + if (Files.exists(target)) { + int counter = 0; + do { + counter++; + target = target.resolveSibling(target.getFileName().toString() + "_" + counter); + } while (Files.exists(target)); + } + var skin = Auth.getInstance().getSkinManager().read(p, false); + if (skin != null) { + Files.write(target, skin.image().join()); + } else { + AxolotlClientCommon.getInstance().getLogger().info("Skipping dragged file {} because it does not seem to be a valid skin!", p); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.not_copied", p.getFileName()); + } + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); + } + }, ThreadExecuter.service()); + } + CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } private @NotNull Entry createEntryForSkin(Skin skin, int entryHeight) { @@ -557,8 +582,48 @@ public Entry(int height, SkinWidget widget, @Nullable String label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { + final class SpriteButton extends VanillaButtonWidget { + private Identifier sprite; + + SpriteButton(String message, ButtonWidget.PressAction action, Identifier sprite) { + super(0, 0, 11, 11, message, action); + this.sprite = sprite; + } + + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int i = 1; + if (!this.active) { + i = 0; + } else if (hovered) { + i = 2; + } + + Identifier tex = ButtonWidgetTextures.get(i); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + minecraft.getTextureManager().bind(sprite); + DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { + + } + } + if (asset instanceof Skin skin) { + var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); + var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); + var slimText = I18n.translate("skins.manage.variant.classic"); + var wideText = I18n.translate("skins.manage.variant.slim"); + actionButtons.add(new SpriteButton(skin.classicVariant() ? wideText : slimText, btn -> { + var self = (SpriteButton) btn; + skin.classicVariant(!skin.classicVariant()); + self.sprite = skin.classicVariant() ? slimSprite : wideSprite; + self.setMessage(skin.classicVariant() ? wideText : slimText); + }, skin.classicVariant() ? slimSprite : wideSprite)); + } if (asset.isLocal()) { - var delete = new VanillaButtonWidget(0, 0, 11, 11, I18n.translate("skins.manage.delete"), btn -> { + this.actionButtons.add(new SpriteButton(I18n.translate("skins.manage.delete"), btn -> { btn.active = false; client.openScreen(new ConfirmScreen((confirmed, i) -> { client.openScreen(SkinManagementScreen.this); @@ -575,33 +640,10 @@ public Entry(int height, SkinWidget widget, @Nullable String label) { AxoText.translatable("skins.manage.delete.confirm.desc_active") : AxoText.translatable("skins.manage.delete.confirm.desc") ).br$color(Colors.RED.toInt())).getFormattedString(), 0)); - }) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/delete.png"); - - @Override - protected void drawWidget(int mouseX, int mouseY, float delta) { - int i = 1; - if (!this.active) { - i = 0; - } else if (hovered) { - i = 2; - } - - Identifier tex = ButtonWidgetTextures.get(i); - DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); - minecraft.getTextureManager().bind(SPRITE); - DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { - - } - }; - this.actionButtons.add(delete); + }, new Identifier("axolotlclient", "textures/gui/sprites/delete.png"))); } if (asset.supportsDownload() && !asset.isLocal()) { - var download = new VanillaButtonWidget(0, 0, 11, 11, I18n.translate("skins.manage.download"), btn -> { + this.actionButtons.add(new SpriteButton(I18n.translate("skins.manage.download"), btn -> { btn.active = false; asset.image().thenAcceptAsync(b -> { try { @@ -614,30 +656,7 @@ protected void drawScrollingText(TextRenderer renderer, int offset, Color color) refreshCurrentList(); btn.active = true; }); - }) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - protected void drawWidget(int mouseX, int mouseY, float delta) { - int i = 1; - if (!this.active) { - i = 0; - } else if (hovered) { - i = 2; - } - - Identifier tex = ButtonWidgetTextures.get(i); - DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); - minecraft.getTextureManager().bind(SPRITE); - DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { - - } - }; - this.actionButtons.add(download); + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png"))); } } if (label != null) { @@ -766,9 +785,7 @@ protected void drawWidget(int mouseX, int mouseY, float partialTick) { button.render(mouseX, mouseY, partialTick); } if (button.isHovered()) { - //GlStateManager.translatef(0, 0, 200); tooltip = button.getMessage(); - //GlStateManager.translatef(0, 0, -400); } actionButtonY += button.getHeight() + 2; } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 9477317b6..5890a236d 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -50,8 +50,12 @@ public class SkinManager { private final Set loadedTextures = new ConcurrentSkipListSet<>(Comparator.comparing(Object::toString)); - @SuppressWarnings("UnstableApiUsage") public Skin read(Path p) { + return read(p, true); + } + + @SuppressWarnings("UnstableApiUsage") + public Skin read(Path p, boolean fix) { boolean slim; String sha256; try { @@ -63,12 +67,17 @@ public Skin read(Path p) { int width = img.getWidth(); if (width != 64) return null; if (height == 32) { - img = new SkinImageProcessor().process(img); - try (var out = Files.newOutputStream(p)) { - ImageIO.write(img, "png", out); + slim = false; + if (fix) { + try (var out = Files.newOutputStream(p)) { + ImageIO.write(new SkinImageProcessor().process(img), "png", out); + } } - } else if (height != 64) return null; - slim = ClientColors.ARGB.alpha(img.getRGB(47, 63)) == 0; + } else if (height != 64) { + return null; + } else { + slim = ClientColors.ARGB.alpha(img.getRGB(63, 63)) == 0; + } } return new Skin.Local(!slim, p, sha256); } catch (Exception e) { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index e31261c66..f353e61d9 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -89,7 +89,9 @@ public static void render(boolean classicVariant, model.rightPants.render(k); model.jacket.render(k); model.renderLeftArm(); - model.renderRightArm(); + model.rightArm.render(0.0625F); + GlStateManager.translatef(0, 0, -0.62F); // why? + model.rightSleeve.render(0.0625F); GlStateManager.popMatrix(); if (cape != null) { GlStateManager.pushMatrix(); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 6a070f5d8..85c78753d 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -84,7 +84,7 @@ protected void drawWidget(int mouseX, int mouseY, float partialTick) { CompletableFuture loader = skin == null ? null : skinManager.loadSkin(skin); if (loader != null && loader.isDone()) { skinRl = loader.join(); - classic = skin.isClassicVariant(); + classic = skin.classicVariant(); } else { var uuid = UUIDHelper.fromUndashed(owner.getUuid()); classic = DefaultSkinUtils.getDefaultModelType(uuid).equals("default"); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index 9d8014950..fa2a6fff8 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -31,6 +31,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; @@ -46,6 +47,7 @@ import io.github.axolotlclient.util.JsonBuilders; import io.github.axolotlclient.util.Logger; import io.github.axolotlclient.util.NetworkUtil; +import lombok.ToString; // Partly oriented on In-Game-Account-Switcher by The-Fireplace, VidTu public class MSApi { @@ -191,11 +193,25 @@ public static MCProfile get(JsonObject json) { .toList()); } - public record OnlineSkin(String id, String state, String url, String variant, - String textureKey) implements Skin { + @ToString + public static final class OnlineSkin implements Skin { public static final String VARIANT_CLASSIC = "CLASSIC"; - //public static final String VARIANT_SLIM = "SLIM"; + public static final String VARIANT_SLIM = "SLIM"; public static final String STATE_ACTIVE = "ACTIVE"; + private final String id; + private final String state; + private final String url; + private boolean classicVariant; + private final String textureKey; + + public OnlineSkin(String id, String state, String url, String variant, + String textureKey) { + this.id = id; + this.state = state; + this.url = url; + this.classicVariant = VARIANT_CLASSIC.equals(variant); + this.textureKey = textureKey; + } public static OnlineSkin get(JsonObject object) { String url = object.get("url").getAsString(); @@ -221,8 +237,13 @@ public CompletableFuture image() { }); } - public boolean isClassicVariant() { - return VARIANT_CLASSIC.equals(variant()); + public boolean classicVariant() { + return classicVariant; + } + + @Override + public void classicVariant(boolean classic) { + this.classicVariant = classic; } public boolean active() { @@ -238,6 +259,45 @@ public CompletableFuture equip(MSApi api, Account account) { public boolean supportsDownload() { return true; } + + public String id() { + return id; + } + + public String state() { + return state; + } + + @Override + public String url() { + return url; + } + + public String variant() { + return classicVariant ? VARIANT_CLASSIC : VARIANT_SLIM; + } + + @Override + public String textureKey() { + return textureKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (OnlineSkin) obj; + return Objects.equals(this.id, that.id) && + Objects.equals(this.state, that.state) && + Objects.equals(this.url, that.url) && + Objects.equals(this.classicVariant, that.classicVariant) && + Objects.equals(this.textureKey, that.textureKey); + } + + @Override + public int hashCode() { + return Objects.hash(id, state, url, classicVariant, textureKey); + } } public record OnlineCape(String id, String state, String url, String alias, String textureKey) implements Cape { @@ -405,7 +465,7 @@ public CompletableFuture uploadAndSetSkin(Account account, Skin.Local .uri(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")) .header("Authorization", "Bearer " + account.getAuthToken()) .POST(MultipartBodyPublisher.newBuilder() - .textPart("variant", skin.isClassicVariant() ? "classic" : "slim") + .textPart("variant", skin.classicVariant() ? "classic" : "slim") .filePart("file", skin.file(), MediaType.IMAGE_PNG).build()).build()) .thenApply(this::extractProfile); } catch (FileNotFoundException e) { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index fdba14c7d..6255cb1f4 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -32,15 +32,34 @@ import io.github.axolotlclient.modules.auth.MSApi; public interface Skin extends Asset { - boolean isClassicVariant(); - - record Local(boolean classic, Path file, String textureKey) implements Skin { + boolean classicVariant(); + void classicVariant(boolean classic); + + final class Local implements Skin { + public static final String METADATA_SUFFIX = ".meta"; + private boolean classic; + private final Path file; + private final String textureKey; + + public Local(boolean classic, Path file, String textureKey) { + this.classic = classic; + this.file = file; + this.textureKey = textureKey; + } @Override - public boolean isClassicVariant() { + public boolean classicVariant() { return classic; } + @Override + public void classicVariant(boolean classic) { + if (classic != this.classic) { + + } + this.classic = classic; + } + @Override public CompletableFuture image() { return CompletableFuture.supplyAsync(() -> { @@ -66,13 +85,28 @@ public CompletableFuture equip(MSApi api, Account account) { public boolean isLocal() { return true; } + + @Override + public Path file() { + return file; + } + + @Override + public String textureKey() { + return textureKey; + } } record Shared(Local local, MSApi.MCProfile.OnlineSkin online) implements Skin { @Override - public boolean isClassicVariant() { - return online.isClassicVariant(); + public boolean classicVariant() { + return local.classicVariant(); + } + + @Override + public void classicVariant(boolean classic) { + local.classicVariant(classic); } @Override @@ -112,7 +146,7 @@ public Path file() { @Override public String url() { - return online().url(); + return online.url(); } @Override diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index 53fe6ab68..f2b8a7bf8 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -828,5 +828,9 @@ "skins.manage.delete.confirm.desc_active": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", "skins.manage.animations": "Skin Manger Animations", "skins.manage.download": "Download Skin", - "skins.manage.import": "Import Skin" + "skins.manage.import": "Import Skins", + "skins.notification.title": "Skin Management", + "skins.notification.not_copied": "Skipped file %s because it does not seem like a valid skin file!", + "skins.manage.variant.classic": "Use Classic (Wide) Variant", + "skins.manage.variant.slim": "Use Slim Variant" } diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/slim.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/slim.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d3c5ce4cfe04fe74f9ae244c802bb16329498e GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhjKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CC3g$*qX48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxD zSIOFxpfsJWS0S#S1(WeLWTtJRY3*my87lwDKbxy8LY7^soK)78&qol`;+0KW-3 AZU6uP literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/wide.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/wide.png new file mode 100644 index 0000000000000000000000000000000000000000..1dd133549455057d3b940c647c100380849219f7 GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhjKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CC3g$*qX48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!CBxD zSO1HA^)&#sGI+ZBxvX Date: Sun, 7 Sep 2025 20:21:53 +0200 Subject: [PATCH 19/23] add local metadata file --- .../auth/skin/SkinManagementScreen.java | 75 +++++++------ .../modules/auth/skin/SkinManager.java | 7 +- .../modules/auth/skin/SkinRenderer.java | 20 +++- .../skins/PlayerSkinTextureAccessor.java | 37 ------- .../auth/skin/SkinManagementScreen.java | 87 ++++++++------- .../modules/auth/skin/SkinManager.java | 69 +++++++++++- .../main/resources/axolotlclient.mixins.json | 1 - .../auth/skin/SkinManagementScreen.java | 16 ++- .../modules/auth/skin/SkinManager.java | 7 +- .../skins/PlayerSkinTextureAccessor.java | 37 ------- .../auth/skin/SkinManagementScreen.java | 16 ++- .../modules/auth/skin/SkinManager.java | 67 +++++++++++- .../main/resources/axolotlclient.mixins.json | 1 - .../auth/skin/SkinManagementScreen.java | 99 ++++++++---------- .../modules/auth/skin/SkinManager.java | 9 +- .../axolotlclient/modules/auth/skin/Skin.java | 34 +++++- .../io/github/axolotlclient/util/Watcher.java | 21 ++-- .../assets/axolotlclient/lang/en_us.json | 3 +- .../textures/gui/sprites/folder.png | Bin 0 -> 217 bytes 19 files changed, 363 insertions(+), 243 deletions(-) delete mode 100644 1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java delete mode 100644 1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java create mode 100644 common/src/main/resources/assets/axolotlclient/textures/gui/sprites/folder.png diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index b646afc54..60edcc16e 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -58,7 +58,6 @@ import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.client.render.Tessellator; import net.minecraft.client.render.VertexFormats; -import net.minecraft.client.resource.language.I18n; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; import net.minecraft.text.MutableText; @@ -90,7 +89,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(new TranslatableText("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -167,23 +166,18 @@ protected MutableText getNarrationMessage() { this.capesTab = true; }); navBar.add(capesTab); - var importButton = new ButtonWidget(capesTab.x+capesTab.getWidth()-11, capesTab.y-13, 11, 11, new TranslatableText("skins.manage.import"), btn -> { + var importButton = new SpriteButton(new TranslatableText("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); - }) { - private final Identifier sprite = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); - DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); - client.getTextureManager().bindTexture(sprite); - drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - if (this.isHovered()) { - tooltip = getMessage(); - } - } - }; + }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); + importButton.x = capesTab.x + capesTab.getWidth() - 11; + importButton.y = capesTab.y - 13; + var downloadButton = new SpriteButton(new TranslatableText("skins.manage.import.online"), btn -> { + btn.active = false; + // TODO + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); + downloadButton.x = importButton.x - 2 - 11; + downloadButton.y = capesTab.y - 13; skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -193,6 +187,7 @@ public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float del addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(downloadButton); addDrawableChild(importButton); addDrawableChild(back); }; @@ -252,7 +247,7 @@ public void render(MatrixStack graphics, int mouseX, int mouseY, float delta) { drawables.forEach(d -> d.render(graphics, mouseX, mouseY, delta)); drawCenteredText(graphics, textRenderer, getTitle(), width / 2, 33 / 2 - textRenderer.fontHeight / 2, -1); if (tooltip != null) { - renderTooltip(graphics, tooltip, mouseX, mouseY+20); + renderTooltip(graphics, tooltip, mouseX, mouseY + 20); } } @@ -384,7 +379,7 @@ public void filesDragged(List packs) { int counter = 0; do { counter++; - target = target.resolveSibling(target.getFileName().toString()+"_"+counter); + target = target.resolveSibling(target.getFileName().toString() + "_" + counter); } while (Files.exists(target)); } var skin = Auth.getInstance().getSkinManager().read(p, false); @@ -563,25 +558,6 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { super(0, 0, widget.getWidth(), height, LiteralText.EMPTY); widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); - class SpriteButton extends ButtonWidget { - private Identifier sprite; - - public SpriteButton(Text message, PressAction onPress, Identifier sprite) { - super(0, 0, 11, 11, message, onPress); - this.sprite = sprite; - } - - @Override - public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { - Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); - DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); - client.getTextureManager().bindTexture(sprite); - drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); - if (this.isHovered()) { - tooltip = getMessage(); - } - } - } if (asset instanceof Skin skin) { var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); @@ -623,6 +599,9 @@ public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float del var out = SKINS_DIR.resolve(asset.textureKey()); Files.createDirectories(out.getParent()); Files.write(out, b); + if (asset instanceof Skin skin) { + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, skin.classicVariant())); + } } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } @@ -812,4 +791,24 @@ public static void render(MatrixStack graphics, int x0, int y0, int x1, int y1, } } } + + private class SpriteButton extends ButtonWidget { + private Identifier sprite; + + public SpriteButton(Text message, PressAction onPress, Identifier sprite) { + super(0, 0, 11, 11, message, onPress); + this.sprite = sprite; + } + + @Override + public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float delta) { + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, x, y, width, height, new DrawUtil.NineSlice(200, 20, 3)); + client.getTextureManager().bindTexture(sprite); + drawTexture(graphics, x + 2, y + 2, 0, 0, 7, 7, 7, 7); + if (this.isHovered()) { + tooltip = getMessage(); + } + } + } } diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 80e1d96bc..a672e8686 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -57,6 +57,7 @@ public Skin read(Path p) { @SuppressWarnings("UnstableApiUsage") public Skin read(Path p, boolean fix) { + if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { @@ -80,7 +81,11 @@ public Skin read(Path p, boolean fix) { } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(50, 16)) == 0; + } + var metadata = Skin.Local.readMetadata(p); + if (metadata != null && metadata.containsKey(Skin.Local.CLASSIC_METADATA_KEY)) { + slim = !(boolean) metadata.get(Skin.Local.CLASSIC_METADATA_KEY); } } } diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 433d0146c..3fa3af0a1 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -22,8 +22,11 @@ package io.github.axolotlclient.modules.auth.skin; +import java.util.function.Consumer; + import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.model.ModelPart; import net.minecraft.client.render.*; import net.minecraft.client.render.entity.model.PlayerEntityModel; import net.minecraft.client.util.math.MatrixStack; @@ -85,7 +88,22 @@ public static void render(MatrixStack graphics, boolean classicVariant, graphics.pop(); } MinecraftClient.getInstance().getTextureManager().bindTexture(skinTexture); - model.render(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(model.getLayer(skinTexture)), 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); + var consumer = VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(model.getLayer(skinTexture)); + Consumer renderModelPart = m -> m.render(graphics, consumer, 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); + renderModelPart.accept(model.head); + renderModelPart.accept(model.torso); + renderModelPart.accept(model.rightArm); + renderModelPart.accept(model.leftArm); + renderModelPart.accept(model.rightLeg); + renderModelPart.accept(model.leftLeg); + renderModelPart.accept(model.helmet); + renderModelPart.accept(model.leftPantLeg); + renderModelPart.accept(model.rightPantLeg); + renderModelPart.accept(model.leftSleeve); + graphics.translate(0, 0, -0.62f); + renderModelPart.accept(model.rightSleeve); + graphics.translate(0, 0, 0.62f); + renderModelPart.accept(model.jacket); tessellator.draw(); graphics.pop(); diff --git a/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java b/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java deleted file mode 100644 index be7933c05..000000000 --- a/1.20/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright © 2025 moehreag & Contributors - * - * This file is part of AxolotlClient. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * For more information, see the LICENSE file. - */ - -package io.github.axolotlclient.mixin.skins; - -import com.mojang.blaze3d.texture.NativeImage; -import net.minecraft.client.texture.PlayerSkinTexture; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(PlayerSkinTexture.class) -public interface PlayerSkinTextureAccessor { - - @Invoker("remapTexture") - static NativeImage invokeRemapTexture(NativeImage img) { - throw new UnsupportedOperationException(); - } -} diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 0d984229d..923f20e66 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -29,7 +29,6 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -80,7 +79,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -157,24 +156,16 @@ protected void updateNarration(NarrationMessageBuilder builder) { this.capesTab = true; }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); - var importButton = new ButtonWidget(capesTab.getX()+capesTab.getWidth()-11, capesTab.getY()-13, 11, 11, Text.translatable("skins.manage.import"), btn -> { + var importButton = new SpriteButton(Text.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); - }, Supplier::get) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(SPRITE, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { - - } - }; - importButton.setTooltip(Tooltip.create(importButton.getMessage())); + }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); + importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); + var downloadButton = new SpriteButton(Text.translatable("skins.manage.import.online"), btn -> { + btn.active = false; + // TODO + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); + downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -185,6 +176,7 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(downloadButton); addDrawableChild(importButton); addDrawableChild(back); }; @@ -349,7 +341,7 @@ public void filesDragged(List packs) { int counter = 0; do { counter++; - target = target.resolveSibling(target.getFileName().toString()+"_"+counter); + target = target.resolveSibling(target.getFileName().toString() + "_" + counter); } while (Files.exists(target)); } var skin = Auth.getInstance().getSkinManager().read(p, false); @@ -535,32 +527,6 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { - class SpriteButton extends ButtonWidget { - private Identifier sprite; - - public SpriteButton(Text message, PressAction onPress, Identifier sprite) { - super(0, 0, 11, 11, message, onPress, DEFAULT_NARRATION); - this.sprite = sprite; - setTooltip(Tooltip.create(message, Text.empty())); - } - - @Override - public void setMessage(Text message) { - super.setMessage(message); - setTooltip(Tooltip.create(message, Text.empty())); - } - - @Override - protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - super.drawWidget(graphics, mouseX, mouseY, delta); - graphics.drawTexture(sprite, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { - - } - } if (asset instanceof Skin skin) { var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); @@ -601,6 +567,9 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int var out = SKINS_DIR.resolve(asset.textureKey()); Files.createDirectories(out.getParent()); Files.write(out, b); + if (asset instanceof Skin skin) { + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, skin.classicVariant())); + } } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } @@ -797,5 +766,33 @@ public static void render(GuiGraphics graphics, int x0, int y0, int x1, int y1, vertexConsumer.vertex(pose, x1, y1, z).color(col1).next(); } } + + } + + private static class SpriteButton extends ButtonWidget { + private Identifier sprite; + + public SpriteButton(Text message, PressAction onPress, Identifier sprite) { + super(0, 0, 11, 11, message, onPress, DEFAULT_NARRATION); + this.sprite = sprite; + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + public void setMessage(Text message) { + super.setMessage(message); + setTooltip(Tooltip.create(message, Text.empty())); + } + + @Override + protected void drawWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.drawWidget(graphics, mouseX, mouseY, delta); + graphics.drawTexture(sprite, getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int color) { + + } } } diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index a76162d07..41af7f223 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,7 +37,6 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; -import io.github.axolotlclient.mixin.skins.PlayerSkinTextureAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.MinecraftClient; @@ -54,6 +53,7 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { + if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { @@ -65,7 +65,7 @@ public Skin read(Path p, boolean fix) { if (width != 64) return null; if (height == 32) { if (fix) { - try (var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img)) { + try (var img2 = remapTexture(img)) { img2.writeFile(p); } } @@ -73,16 +73,77 @@ public Skin read(Path p, boolean fix) { } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(50, 16)) == 0; + } + var metadata = Skin.Local.readMetadata(p); + if (metadata != null && metadata.containsKey(Skin.Local.CLASSIC_METADATA_KEY)) { + slim = !(boolean) metadata.get(Skin.Local.CLASSIC_METADATA_KEY); } } return new Skin.Local(!slim, p, sha256); } catch (Exception e) { - AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: ", e); + AxolotlClientCommon.getInstance().getLogger().warn("Failed to probe skin: {}", p, e); } return null; } + private static NativeImage remapTexture(NativeImage skinImage) { + boolean legacySkin = skinImage.getHeight() == 32; + if (legacySkin) { + NativeImage nativeImage = new NativeImage(64, 64, true); + nativeImage.copyFrom(skinImage); + skinImage.close(); + skinImage = nativeImage; + nativeImage.fillRect(0, 32, 64, 32, 0); + nativeImage.copyRectangle(4, 16, 16, 32, 4, 4, true, false); + nativeImage.copyRectangle(8, 16, 16, 32, 4, 4, true, false); + nativeImage.copyRectangle(0, 20, 24, 32, 4, 12, true, false); + nativeImage.copyRectangle(4, 20, 16, 32, 4, 12, true, false); + nativeImage.copyRectangle(8, 20, 8, 32, 4, 12, true, false); + nativeImage.copyRectangle(12, 20, 16, 32, 4, 12, true, false); + nativeImage.copyRectangle(44, 16, -8, 32, 4, 4, true, false); + nativeImage.copyRectangle(48, 16, -8, 32, 4, 4, true, false); + nativeImage.copyRectangle(40, 20, 0, 32, 4, 12, true, false); + nativeImage.copyRectangle(44, 20, -8, 32, 4, 12, true, false); + nativeImage.copyRectangle(48, 20, -16, 32, 4, 12, true, false); + nativeImage.copyRectangle(52, 20, -8, 32, 4, 12, true, false); + } + + stripAlpha(skinImage, 0, 0, 32, 16); + if (legacySkin) { + stripColor(skinImage, 32, 0, 64, 32); + } + + stripAlpha(skinImage, 0, 16, 64, 32); + stripAlpha(skinImage, 16, 48, 48, 64); + return skinImage; + } + + @SuppressWarnings("SameParameterValue") + private static void stripColor(NativeImage image, int x1, int y1, int x2, int y2) { + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + int k = image.getPixelColor(x, y); + if ((k >> 24 & 0xFF) < 128) { + return; + } + } + } + + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + image.setPixelColor(x, y, image.getPixelColor(x, y) & 16777215); + } + } + } + + private static void stripAlpha(NativeImage image, int x1, int y1, int x2, int y2) { + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + image.setPixelColor(x, y, image.getPixelColor(x, y) | 0xFF000000); + } + } + } public CompletableFuture loadSkin(Skin skin) { var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); diff --git a/1.20/src/main/resources/axolotlclient.mixins.json b/1.20/src/main/resources/axolotlclient.mixins.json index 89f22e672..b8e2bce7c 100644 --- a/1.20/src/main/resources/axolotlclient.mixins.json +++ b/1.20/src/main/resources/axolotlclient.mixins.json @@ -61,7 +61,6 @@ "WorldRendererAccessor", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", - "skins.PlayerSkinTextureAccessor", "translation.TranslationStorageMixin" ], "injectors": { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index a05b9a2af..9b151fa39 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -88,7 +88,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Component.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -151,12 +151,18 @@ protected void init() { this.capesTab = true; }).pos(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); - var importButton = SpriteIconButton.builder(Component.translatable("skins.manage.import"), btn -> { + var importButton = SpriteIconButton.builder(Component.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::onFilesDrop).thenRun(() -> btn.active = true); - }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "folder"), 7, 7).size(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); + var downloadButton = SpriteIconButton.builder(Component.translatable("skins.manage.import.online"), btn -> { + btn.active = false; + // TODO + }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); + downloadButton.setTooltip(Tooltip.create(downloadButton.getMessage())); + downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -167,6 +173,7 @@ protected void init() { addRenderableWidget(capesTab); addRenderableWidget(skinList); addRenderableWidget(capesList); + addRenderableWidget(downloadButton); addRenderableWidget(importButton); addRenderableWidget(back); }; @@ -558,6 +565,9 @@ public void renderString(GuiGraphics guiGraphics, Font font, int color) { var out = SKINS_DIR.resolve(asset.textureKey()); Files.createDirectories(out.getParent()); Files.write(out, b); + if (asset instanceof Skin skin) { + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, skin.classicVariant())); + } } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 891d884e5..e748da430 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -54,6 +54,7 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { + if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { @@ -73,7 +74,11 @@ public Skin read(Path p, boolean fix) { } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixel(63, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixel(50, 16)) == 0; + } + var metadata = Skin.Local.readMetadata(p); + if (metadata != null && metadata.containsKey(Skin.Local.CLASSIC_METADATA_KEY)) { + slim = !(boolean) metadata.get(Skin.Local.CLASSIC_METADATA_KEY); } } return new Skin.Local(!slim, p, sha256); diff --git a/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java b/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java deleted file mode 100644 index be7933c05..000000000 --- a/1.21/src/main/java/io/github/axolotlclient/mixin/skins/PlayerSkinTextureAccessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright © 2025 moehreag & Contributors - * - * This file is part of AxolotlClient. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * - * For more information, see the LICENSE file. - */ - -package io.github.axolotlclient.mixin.skins; - -import com.mojang.blaze3d.texture.NativeImage; -import net.minecraft.client.texture.PlayerSkinTexture; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(PlayerSkinTexture.class) -public interface PlayerSkinTextureAccessor { - - @Invoker("remapTexture") - static NativeImage invokeRemapTexture(NativeImage img) { - throw new UnsupportedOperationException(); - } -} diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 34c75aa30..fcad07f9d 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -88,7 +88,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -150,12 +150,18 @@ protected void init() { this.capesTab = true; }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); navBar.add(capesTab); - var importButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import"), btn -> { + var importButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); - }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); + }, true).sprite(Identifier.of("axolotlclient", "folder"), 7, 7).dimensions(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); + var downloadButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import.online"), btn -> { + btn.active = false; + // TODO + }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); + downloadButton.setTooltip(Tooltip.create(downloadButton.getMessage())); + downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -166,6 +172,7 @@ protected void init() { addDrawableSelectableElement(capesTab); addDrawableSelectableElement(skinList); addDrawableSelectableElement(capesList); + addDrawableSelectableElement(downloadButton); addDrawableSelectableElement(importButton); addDrawableSelectableElement(back); }; @@ -565,6 +572,9 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int var out = SKINS_DIR.resolve(asset.textureKey()); Files.createDirectories(out.getParent()); Files.write(out, b); + if (asset instanceof Skin skin) { + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, skin.classicVariant())); + } } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index b59112311..afb272c82 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -37,7 +37,6 @@ import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.AxoMinecraftClient; import io.github.axolotlclient.bridge.util.AxoIdentifier; -import io.github.axolotlclient.mixin.skins.PlayerSkinTextureAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.util.ClientColors; import net.minecraft.client.MinecraftClient; @@ -54,6 +53,7 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { + if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { @@ -65,7 +65,7 @@ public Skin read(Path p, boolean fix) { if (width != 64) return null; if (height == 32) { if (fix) { - try (var img2 = PlayerSkinTextureAccessor.invokeRemapTexture(img)) { + try (var img2 = remapTexture(img)) { img2.writeFile(p); } } @@ -73,7 +73,11 @@ public Skin read(Path p, boolean fix) { } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getPixelColor(63, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getPixelColor(50, 16)) == 0; + } + var metadata = Skin.Local.readMetadata(p); + if (metadata != null && metadata.containsKey(Skin.Local.CLASSIC_METADATA_KEY)) { + slim = !(boolean) metadata.get(Skin.Local.CLASSIC_METADATA_KEY); } } return new Skin.Local(!slim, p, sha256); @@ -83,6 +87,63 @@ public Skin read(Path p, boolean fix) { return null; } + private static NativeImage remapTexture(NativeImage skinImage) { + boolean legacySkin = skinImage.getHeight() == 32; + if (legacySkin) { + NativeImage nativeImage = new NativeImage(64, 64, true); + nativeImage.copyFrom(skinImage); + skinImage.close(); + skinImage = nativeImage; + nativeImage.fillRect(0, 32, 64, 32, 0); + nativeImage.copyRectangle(4, 16, 16, 32, 4, 4, true, false); + nativeImage.copyRectangle(8, 16, 16, 32, 4, 4, true, false); + nativeImage.copyRectangle(0, 20, 24, 32, 4, 12, true, false); + nativeImage.copyRectangle(4, 20, 16, 32, 4, 12, true, false); + nativeImage.copyRectangle(8, 20, 8, 32, 4, 12, true, false); + nativeImage.copyRectangle(12, 20, 16, 32, 4, 12, true, false); + nativeImage.copyRectangle(44, 16, -8, 32, 4, 4, true, false); + nativeImage.copyRectangle(48, 16, -8, 32, 4, 4, true, false); + nativeImage.copyRectangle(40, 20, 0, 32, 4, 12, true, false); + nativeImage.copyRectangle(44, 20, -8, 32, 4, 12, true, false); + nativeImage.copyRectangle(48, 20, -16, 32, 4, 12, true, false); + nativeImage.copyRectangle(52, 20, -8, 32, 4, 12, true, false); + } + + stripAlpha(skinImage, 0, 0, 32, 16); + if (legacySkin) { + stripColor(skinImage, 32, 0, 64, 32); + } + + stripAlpha(skinImage, 0, 16, 64, 32); + stripAlpha(skinImage, 16, 48, 48, 64); + return skinImage; + } + + @SuppressWarnings("SameParameterValue") + private static void stripColor(NativeImage image, int x1, int y1, int x2, int y2) { + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + int k = image.getPixelColor(x, y); + if ((k >> 24 & 0xFF) < 128) { + return; + } + } + } + + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + image.setPixelColor(x, y, image.getPixelColor(x, y) & 16777215); + } + } + } + + private static void stripAlpha(NativeImage image, int x1, int y1, int x2, int y2) { + for (int x = x1; x < x2; x++) { + for (int y = y1; y < y2; y++) { + image.setPixelColor(x, y, image.getPixelColor(x, y) | 0xFF000000); + } + } + } public CompletableFuture loadSkin(Skin skin) { var rl = AxoIdentifier.of(AxolotlClientCommon.MODID, "skins/" + skin.textureKey()); diff --git a/1.21/src/main/resources/axolotlclient.mixins.json b/1.21/src/main/resources/axolotlclient.mixins.json index dec975510..2491658dd 100644 --- a/1.21/src/main/resources/axolotlclient.mixins.json +++ b/1.21/src/main/resources/axolotlclient.mixins.json @@ -60,7 +60,6 @@ "WorldRendererAccessor", "WorldRendererMixin", "api.JoinMulitplayerScreenMixin", - "skins.PlayerSkinTextureAccessor", "translation.TranslationStorageMixin" ], "injectors": { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index a4c5f0e3e..1ce79af57 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -39,7 +39,6 @@ import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Color; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; -import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ButtonWidget; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ClickableWidget; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.Element; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ParentElement; @@ -86,7 +85,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(I18n.translate("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -183,33 +182,18 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { this.capesTab = true; }); navBar.add(capesTab); - var importButton = new VanillaButtonWidget(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13, 11, 11, I18n.translate("skins.manage.import"), btn -> { + var importButton = new SpriteButton(I18n.translate("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::onFileDrop).thenRun(() -> btn.active = true); - }) { - private final Identifier SPRITE = new Identifier("axolotlclient", "textures/gui/sprites/download.png"); - - @Override - protected void drawWidget(int mouseX, int mouseY, float delta) { - int i = 1; - if (!this.active) { - i = 0; - } else if (hovered) { - tooltip = getMessage(); - i = 2; - } - - Identifier tex = ButtonWidgetTextures.get(i); - DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); - minecraft.getTextureManager().bind(SPRITE); - DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { - - } - }; + }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); + importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); + importButton.setY(capesTab.getY() - 13); + var downloadButton = new SpriteButton(I18n.translate("skins.manage.import.online"), btn -> { + btn.active = false; + // TODO + }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); + downloadButton.setX(importButton.getX() - 2 - 11); + downloadButton.setY(capesTab.getY() - 13); skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -220,6 +204,7 @@ protected void drawScrollingText(TextRenderer renderer, int offset, Color color) addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); + addDrawableChild(downloadButton); addDrawableChild(importButton); addDrawableChild(back); }; @@ -582,34 +567,6 @@ public Entry(int height, SkinWidget widget, @Nullable String label) { widget.setWidth(getWidth() - 4); var asset = widget.getFocusedAsset(); if (asset != null) { - final class SpriteButton extends VanillaButtonWidget { - private Identifier sprite; - - SpriteButton(String message, ButtonWidget.PressAction action, Identifier sprite) { - super(0, 0, 11, 11, message, action); - this.sprite = sprite; - } - - @Override - protected void drawWidget(int mouseX, int mouseY, float delta) { - int i = 1; - if (!this.active) { - i = 0; - } else if (hovered) { - i = 2; - } - - Identifier tex = ButtonWidgetTextures.get(i); - DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); - minecraft.getTextureManager().bind(sprite); - DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); - } - - @Override - protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { - - } - } if (asset instanceof Skin skin) { var wideSprite = new Identifier("axolotlclient", "textures/gui/sprites/wide.png"); var slimSprite = new Identifier("axolotlclient", "textures/gui/sprites/slim.png"); @@ -650,6 +607,9 @@ protected void drawScrollingText(TextRenderer renderer, int offset, Color color) var out = SKINS_DIR.resolve(asset.textureKey()); Files.createDirectories(out.getParent()); Files.write(out, b); + if (asset instanceof Skin skin) { + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, skin.classicVariant())); + } } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to download: ", e); } @@ -853,4 +813,33 @@ public static void render(int x0, int y0, int x1, int y1, float gradientWidth, i } } } + + private class SpriteButton extends VanillaButtonWidget { + private Identifier sprite; + + SpriteButton(String message, PressAction action, Identifier sprite) { + super(0, 0, 11, 11, message, action); + this.sprite = sprite; + } + + @Override + protected void drawWidget(int mouseX, int mouseY, float delta) { + int i = 1; + if (!this.active) { + i = 0; + } else if (hovered) { + i = 2; + } + + Identifier tex = ButtonWidgetTextures.get(i); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + minecraft.getTextureManager().bind(sprite); + DrawUtil.drawTexture(getX() + 2, getY() + 2, 0, 0, 7, 7, 7, 7); + } + + @Override + protected void drawScrollingText(TextRenderer renderer, int offset, Color color) { + + } + } } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 5890a236d..e541ec320 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -56,6 +56,7 @@ public Skin read(Path p) { @SuppressWarnings("UnstableApiUsage") public Skin read(Path p, boolean fix) { + if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { @@ -67,16 +68,20 @@ public Skin read(Path p, boolean fix) { int width = img.getWidth(); if (width != 64) return null; if (height == 32) { - slim = false; if (fix) { try (var out = Files.newOutputStream(p)) { ImageIO.write(new SkinImageProcessor().process(img), "png", out); } } + slim = false; } else if (height != 64) { return null; } else { - slim = ClientColors.ARGB.alpha(img.getRGB(63, 63)) == 0; + slim = ClientColors.ARGB.alpha(img.getRGB(50, 16)) == 0; + } + var metadata = Skin.Local.readMetadata(p); + if (metadata != null && metadata.containsKey(Skin.Local.CLASSIC_METADATA_KEY)) { + slim = !(boolean) metadata.get(Skin.Local.CLASSIC_METADATA_KEY); } } return new Skin.Local(!slim, p, sha256); diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index 6255cb1f4..6f4a998b3 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -23,20 +23,26 @@ package io.github.axolotlclient.modules.auth.skin; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.MSApi; +import io.github.axolotlclient.util.GsonHelper; public interface Skin extends Asset { boolean classicVariant(); + void classicVariant(boolean classic); final class Local implements Skin { public static final String METADATA_SUFFIX = ".meta"; + public static final String CLASSIC_METADATA_KEY = "variant_classic"; private boolean classic; private final Path file; private final String textureKey; @@ -47,6 +53,29 @@ public Local(boolean classic, Path file, String textureKey) { this.textureKey = textureKey; } + @SuppressWarnings("unchecked") + public static Map readMetadata(Path skinFile) { + var metadataFile = skinFile.resolveSibling(skinFile.getFileName().toString() + METADATA_SUFFIX); + if (!Files.exists(metadataFile)) return null; + + try (var in = Files.newInputStream(metadataFile)) { + return (Map) GsonHelper.read(in); + } catch (IOException ignored) { + + } + return null; + } + + public static void writeMetadata(Path skinFile, Object metadata) { + var metadataFile = skinFile.resolveSibling(skinFile.getFileName().toString() + METADATA_SUFFIX); + try (var out = Files.newOutputStream(metadataFile); + var writer = new OutputStreamWriter(out)) { + GsonHelper.GSON.toJson(metadata, writer); + } catch (IOException ignored) { + + } + } + @Override public boolean classicVariant() { return classic; @@ -55,7 +84,10 @@ public boolean classicVariant() { @Override public void classicVariant(boolean classic) { if (classic != this.classic) { - + var metadata = readMetadata(file()); + if (metadata == null) metadata = new HashMap<>(); + metadata.put(CLASSIC_METADATA_KEY, classic); + writeMetadata(file(), metadata); } this.classic = classic; } diff --git a/common/src/main/java/io/github/axolotlclient/util/Watcher.java b/common/src/main/java/io/github/axolotlclient/util/Watcher.java index 4993aed0f..5cedec876 100644 --- a/common/src/main/java/io/github/axolotlclient/util/Watcher.java +++ b/common/src/main/java/io/github/axolotlclient/util/Watcher.java @@ -27,6 +27,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import io.github.axolotlclient.AxolotlClientCommon; import org.jetbrains.annotations.Nullable; @@ -36,9 +37,11 @@ public class Watcher implements AutoCloseable { private static final ScheduledExecutorService thread = Executors.newSingleThreadScheduledExecutor(); private final WatchService watcher; private final Path path; + private final Predicate fileFilter; - public Watcher(Path root) throws IOException { + public Watcher(Path root, Predicate fileFilter) throws IOException { this.path = root; + this.fileFilter = fileFilter; this.watcher = path.getFileSystem().newWatchService(); try { @@ -58,10 +61,10 @@ public Watcher(Path root) throws IOException { } @Nullable - public static Watcher create(Path path) { + public static Watcher create(Path path, Predicate fileFilter) { try { Files.createDirectories(path); - return new Watcher(path); + return new Watcher(path, fileFilter); } catch (IOException var2) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to initialize directory {} monitoring", path, var2); return null; @@ -69,7 +72,11 @@ public static Watcher create(Path path) { } public static Watcher createSelfTicking(Path path, Runnable onUpdate) { - var watcher = create(path); + return createSelfTicking(path, s -> true, onUpdate); + } + + public static Watcher createSelfTicking(Path path, Predicate fileFilter, Runnable onUpdate) { + var watcher = create(path, fileFilter); if (watcher != null) { thread.scheduleAtFixedRate(() -> { try { @@ -97,12 +104,8 @@ public boolean pollForChanges() throws IOException { WatchKey watchKey; while ((watchKey = this.watcher.poll()) != null) { for (WatchEvent watchEvent : watchKey.pollEvents()) { - bl = true; if (watchKey.watchable() == this.path && watchEvent.context() != null) { - Path path = this.path.resolve((Path) watchEvent.context()); - if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { - this.watchDir(path); - } + bl |= this.fileFilter.test(((Path)watchEvent.context()).toString()); } } diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index f2b8a7bf8..b1145cb0a 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -828,7 +828,8 @@ "skins.manage.delete.confirm.desc_active": "This Skin's file will be deleted permanently!\n Are you sure? The skin will not be un-equipped.", "skins.manage.animations": "Skin Manger Animations", "skins.manage.download": "Download Skin", - "skins.manage.import": "Import Skins", + "skins.manage.import.local": "Import Skins", + "skins.manage.import.online": "Download Skin", "skins.notification.title": "Skin Management", "skins.notification.not_copied": "Skipped file %s because it does not seem like a valid skin file!", "skins.manage.variant.classic": "Use Classic (Wide) Variant", diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/folder.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..d36da2fa11b882a7b3e8559b517c57a4882c3d6d GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJgeK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a z5n0T@z;^_M8K-LVNdpDhOFVsD*!}aSY+Op3K9|{rh|aOW4|| zqa4Y`i@cSX#Y!62tkJQU{qaB}vqh Date: Mon, 8 Sep 2025 16:08:32 +0200 Subject: [PATCH 20/23] implement Skin downloads - fix some bugs --- .../mixin/ConfigVanillaButtonWidgetMixin.java | 61 ++++++++++++ .../auth/skin/SkinManagementScreen.java | 82 ++++++++++++---- .../modules/auth/skin/SkinManager.java | 25 ++--- .../modules/auth/skin/SkinRenderer.java | 21 +++-- .../modules/auth/skin/SkinWidget.java | 3 - .../main/resources/axolotlclient.mixins.json | 1 + .../auth/skin/SkinManagementScreen.java | 82 ++++++++++++---- .../auth/skin/SkinManagementScreen.java | 82 ++++++++++++---- .../auth/skin/SkinManagementScreen.java | 82 ++++++++++++---- .../bridge/mixin/util/StyleMixin.java | 34 +++++-- .../mixin/ConfigVanillaButtonWidgetMixin.java | 52 +++++++++++ .../mixin/ConfirmScreenMixin.java | 43 +++++++++ .../mixin/TextRenderUtilsMixin.java | 39 +++++--- .../auth/skin/SkinManagementScreen.java | 88 +++++++++++++----- .../main/resources/axolotlclient.mixins.json | 2 + .../axolotlclient/modules/auth/MSApi.java | 42 +++++++++ .../assets/axolotlclient/lang/en_us.json | 7 +- .../textures/gui/sprites/download.png | Bin 219 -> 222 bytes 18 files changed, 603 insertions(+), 143 deletions(-) create mode 100644 1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java create mode 100644 1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfirmScreenMixin.java diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java new file mode 100644 index 000000000..1832b6a56 --- /dev/null +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin; + +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.widgets.VanillaButtonWidget; +import io.github.axolotlclient.modules.hud.util.DrawUtil; +import io.github.axolotlclient.util.ButtonWidgetTextures; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(VanillaButtonWidget.class) +public abstract class ConfigVanillaButtonWidgetMixin extends ButtonWidget { + @Shadow(remap = false) + public abstract int getX(); + + @Shadow(remap = false) + public abstract int getY(); + + private ConfigVanillaButtonWidgetMixin(int x, int y, int width, int height, Text message, PressAction action) { + super(x, y, width, height, message, action); + } + + @Redirect(method = "renderButton", at = @At(value = "INVOKE", target = "Lio/github/axolotlclient/AxolotlClientConfig/impl/ui/vanilla/widgets/VanillaButtonWidget;drawTexture(Lnet/minecraft/client/util/math/MatrixStack;IIIIII)V", ordinal = 0)) + private void drawTexture$1(VanillaButtonWidget instance, MatrixStack stack, int x, int y, int u, int v, int width, int height) { + + } + + @Redirect(method = "renderButton", at = @At(value = "INVOKE", target = "Lio/github/axolotlclient/AxolotlClientConfig/impl/ui/vanilla/widgets/VanillaButtonWidget;drawTexture(Lnet/minecraft/client/util/math/MatrixStack;IIIIII)V", ordinal = 1)) + private void drawTexture$2$replaceWithNineSlice(VanillaButtonWidget instance, MatrixStack stack, int x, int y, int u, int v, int width, int height) { + Identifier tex = ButtonWidgetTextures.get(getYImage(hovered)); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + MinecraftClient.getInstance().getTextureManager().bindTexture(WIDGETS_LOCATION); + } +} diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 60edcc16e..f08d7817c 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -35,13 +35,14 @@ import com.mojang.blaze3d.systems.RenderSystem; import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.api.SimpleTextInputScreen; +import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.modules.hud.util.DrawUtil; import io.github.axolotlclient.util.ButtonWidgetTextures; import io.github.axolotlclient.util.ClientColors; -import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; @@ -146,7 +147,7 @@ protected MutableText getNarrationMessage() { skinList.visible = skinList.active = false; } List navBar = new ArrayList<>(); - var skinsTab = new ButtonWidget(width * 3 / 4 - 102, headerHeight, 100, 20, new TranslatableText("skins.nav.skins"), btn -> { + var skinsTab = new ButtonWidget(Math.max(width * 3 / 4 - 102, width / 2 + 2), headerHeight, Math.min(100, width / 4 - 2), 20, new TranslatableText("skins.nav.skins"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); @@ -156,7 +157,7 @@ protected MutableText getNarrationMessage() { capesTab = false; }); navBar.add(skinsTab); - var capesTab = new ButtonWidget(width * 3 / 4 + 2, headerHeight, 100, 20, new TranslatableText("skins.nav.capes"), btn -> { + var capesTab = new ButtonWidget(width * 3 / 4 + 2, headerHeight, Math.min(100, width / 4 - 2), 20, new TranslatableText("skins.nav.capes"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); @@ -170,14 +171,23 @@ protected MutableText getNarrationMessage() { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); - importButton.x = capesTab.x + capesTab.getWidth() - 11; - importButton.y = capesTab.y - 13; var downloadButton = new SpriteButton(new TranslatableText("skins.manage.import.online"), btn -> { btn.active = false; - // TODO + promptForSkinDownload(); }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); downloadButton.x = importButton.x - 2 - 11; downloadButton.y = capesTab.y - 13; + if (width - (capesTab.x + capesTab.getWidth()) > 28) { + importButton.x = width - importButton.getWidth() - 2; + downloadButton.x = importButton.x - downloadButton.getWidth() - 2; + importButton.y = capesTab.y + capesTab.getHeight() - 11; + downloadButton.y = importButton.y; + } else { + importButton.x = capesTab.x + capesTab.getWidth() - 11; + importButton.y = capesTab.y - 13; + downloadButton.x = importButton.x - 2 - 11; + downloadButton.y = importButton.y; + } skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -229,6 +239,39 @@ public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float del }); } + private void promptForSkinDownload() { + client.openScreen(new SimpleTextInputScreen(this, new TranslatableText("skins.manage.import.online"), new TranslatableText("skins.manage.import.online.input"), s -> + UUIDHelper.ensureUuidOpt(s).thenAccept(o -> { + if (o.isPresent()) { + AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get()); + Auth.getInstance().getMsApi().getTextures(o.get()) + .exceptionally(th -> { + AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th); + return null; + }).thenAccept(t -> { + if (t == null) { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s); + return; + } + try { + var bytes = t.skin().join(); + var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Files.write(out, bytes); + client.execute(this::loadSkinsList); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); + AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get()); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name()); + } + }); + } else { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s); + } + }))); + } + private T addDrawableChild(T child) { drawables.add(child); return addChild(child); @@ -365,6 +408,17 @@ private void populateSkinList(List skins, int columns) { } } + private Path ensureNonexistent(Path p) { + if (Files.exists(p)) { + int counter = 0; + do { + counter++; + p = p.resolveSibling(p.getFileName().toString() + "_" + counter); + } while (Files.exists(p)); + } + return p; + } + @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; @@ -374,14 +428,7 @@ public void filesDragged(List packs) { Path p = packs.get(i); futs[i] = CompletableFuture.runAsync(() -> { try { - var target = SKINS_DIR.resolve(p.getFileName()); - if (Files.exists(target)) { - int counter = 0; - do { - counter++; - target = target.resolveSibling(target.getFileName().toString() + "_" + counter); - } while (Files.exists(target)); - } + var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName())); var skin = Auth.getInstance().getSkinManager().read(p, false); if (skin != null) { Files.write(target, skin.image().join()); @@ -392,7 +439,7 @@ public void filesDragged(List packs) { } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); } - }, ThreadExecuter.service()); + }, client); } CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } @@ -579,6 +626,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { if (confirmed) { try { Files.delete(asset.file()); + Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); @@ -701,6 +749,8 @@ private float applyEasing(float x) { public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) { int y = this.y + 4; int x = this.x + 2; + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); if (skinWidget.isEquipped() || equipping) { long prog; if (Auth.getInstance().skinManagerAnimations.get()) { @@ -719,8 +769,6 @@ public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float gradientWidth, equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); } - skinWidget.setPosition(x, y); - skinWidget.setWidth(getWidth() - 4); skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); int actionButtonY = this.y + 2; for (var button : actionButtons) { diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index a672e8686..d2ef6265e 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -22,9 +22,9 @@ package io.github.axolotlclient.modules.auth.skin; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; @@ -45,7 +45,6 @@ import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.client.util.DefaultSkinHelper; import net.minecraft.util.Identifier; -import org.lwjgl.system.MemoryStack; public class SkinManager { @@ -63,11 +62,8 @@ public Skin read(Path p, boolean fix) { try { var in = Files.readAllBytes(p); sha256 = Hashing.sha256().hashBytes(in).toString(); - try (MemoryStack memoryStack = MemoryStack.stackPush()) { - ByteBuffer byteBuffer = memoryStack.malloc(in.length); - byteBuffer.put(in); - byteBuffer.rewind(); - try (var img = NativeImage.read(byteBuffer)) { + try (var stream = new ByteArrayInputStream(in)) { + try (var img = NativeImage.read(stream)) { int width = img.getWidth(); int height = img.getHeight(); if (width != 64) return null; @@ -103,11 +99,9 @@ public CompletableFuture loadSkin(Skin skin) { } return skin.image().thenApplyAsync(bytes -> { - try (MemoryStack memoryStack = MemoryStack.stackPush()) { - ByteBuffer byteBuffer = memoryStack.malloc(bytes.length); - byteBuffer.put(bytes); - byteBuffer.rewind(); - var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer)); + try (var stream = new ByteArrayInputStream(bytes)) { + var tex = new NativeImageBackedTexture(NativeImage.read(stream)); + tex.upload(); MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); } catch (IOException e) { throw new UncheckedIOException(e); @@ -129,11 +123,8 @@ public AxoIdentifier loadCape(Cape cape) { } return cape.image().thenApplyAsync(bytes -> { - try (MemoryStack memoryStack = MemoryStack.stackPush()) { - ByteBuffer byteBuffer = memoryStack.malloc(bytes.length); - byteBuffer.put(bytes); - byteBuffer.rewind(); - var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer)); + try (var stream = new ByteArrayInputStream(bytes)) { + var tex = new NativeImageBackedTexture(NativeImage.read(stream)); MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex); } catch (IOException e) { throw new UncheckedIOException(e); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java index 3fa3af0a1..aa18c8668 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java @@ -77,16 +77,7 @@ public static void render(MatrixStack graphics, boolean classicVariant, var tessellator = Tessellator.getInstance(); RenderSystem.enableDepthTest(); RenderSystem.enableBlend(); - if (cape != null) { - graphics.push(); - MinecraftClient.getInstance().getTextureManager().bindTexture(cape); - graphics.translate(0.0F, 0.0F, 0.125F); - graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F)); - graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F)); - model.renderCape(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); - tessellator.draw(); - graphics.pop(); - } + RenderSystem.enableTexture(); MinecraftClient.getInstance().getTextureManager().bindTexture(skinTexture); var consumer = VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(model.getLayer(skinTexture)); Consumer renderModelPart = m -> m.render(graphics, consumer, 15728880, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1); @@ -105,6 +96,16 @@ public static void render(MatrixStack graphics, boolean classicVariant, graphics.translate(0, 0, 0.62f); renderModelPart.accept(model.jacket); tessellator.draw(); + if (cape != null) { + graphics.push(); + MinecraftClient.getInstance().getTextureManager().bindTexture(cape); + graphics.translate(0.0F, 0.0F, 0.125F); + graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F)); + graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F)); + model.renderCape(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV); + tessellator.draw(); + graphics.pop(); + } graphics.pop(); graphics.pop(); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 10a0e265c..ee19b52cd 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -31,7 +31,6 @@ import io.github.axolotlclient.modules.auth.MSApi; import lombok.Getter; import lombok.Setter; -import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.widget.AbstractButtonWidget; import net.minecraft.client.sound.SoundManager; import net.minecraft.client.util.DefaultSkinHelper; @@ -79,8 +78,6 @@ public void noCape(boolean noCapeActive) { @Override public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) { - var minecraft = MinecraftClient.getInstance(); - float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT; float pivotY = -1.0625F; diff --git a/1.16_combat-6/src/main/resources/axolotlclient.mixins.json b/1.16_combat-6/src/main/resources/axolotlclient.mixins.json index 26c026cd2..bb9947a9d 100644 --- a/1.16_combat-6/src/main/resources/axolotlclient.mixins.json +++ b/1.16_combat-6/src/main/resources/axolotlclient.mixins.json @@ -17,6 +17,7 @@ "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", "ClientWorldMixin", + "ConfigVanillaButtonWidgetMixin", "DebugHudMixin", "DownloadingTerrainScreenMixin", "EmitterParticleMixin", diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 923f20e66..95e407a82 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -34,11 +34,12 @@ import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.api.SimpleTextInputScreen; +import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; -import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; @@ -144,7 +145,7 @@ protected void updateNarration(NarrationMessageBuilder builder) { skinList.visible = skinList.active = true; capesList.visible = capesList.active = false; capesTab = false; - }).position(width * 3 / 4 - 102, headerHeight).width(100).build(); + }).position(Math.max(width * 3 / 4 - 102, width / 2 + 2), headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(skinsTab); var capesTab = ButtonWidget.builder(Text.translatable("skins.nav.capes"), btn -> { navBar.forEach(w -> { @@ -154,18 +155,27 @@ protected void updateNarration(NarrationMessageBuilder builder) { skinList.visible = skinList.active = false; capesList.visible = capesList.active = true; this.capesTab = true; - }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); + }).position(width * 3 / 4 + 2, headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(capesTab); var importButton = new SpriteButton(Text.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); - importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); var downloadButton = new SpriteButton(Text.translatable("skins.manage.import.online"), btn -> { btn.active = false; - // TODO + promptForSkinDownload(); }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); - downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); + if (width - (capesTab.getX() + capesTab.getWidth()) > 28) { + importButton.setX(width - importButton.getWidth() - 2); + downloadButton.setX(importButton.getX() - downloadButton.getWidth() - 2); + importButton.setY(capesTab.getY() + capesTab.getHeight() - 11); + downloadButton.setY(importButton.getY()); + } else { + importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); + importButton.setY(capesTab.getY() - 13); + downloadButton.setX(importButton.getX() - 2 - 11); + downloadButton.setY(importButton.getY()); + } skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -207,6 +217,39 @@ protected void updateNarration(NarrationMessageBuilder builder) { }); } + private void promptForSkinDownload() { + client.setScreen(new SimpleTextInputScreen(this, Text.translatable("skins.manage.import.online"), Text.translatable("skins.manage.import.online.input"), s -> + UUIDHelper.ensureUuidOpt(s).thenAccept(o -> { + if (o.isPresent()) { + AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get()); + Auth.getInstance().getMsApi().getTextures(o.get()) + .exceptionally(th -> { + AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th); + return null; + }).thenAccept(t -> { + if (t == null) { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s); + return; + } + try { + var bytes = t.skin().join(); + var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Files.write(out, bytes); + client.execute(this::loadSkinsList); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); + AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get()); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name()); + } + }); + } else { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s); + } + }))); + } + @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { renderBackground(graphics); @@ -327,6 +370,17 @@ private void populateSkinList(List skins, int columns) { } } + private Path ensureNonexistent(Path p) { + if (Files.exists(p)) { + int counter = 0; + do { + counter++; + p = p.resolveSibling(p.getFileName().toString() + "_" + counter); + } while (Files.exists(p)); + } + return p; + } + @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; @@ -336,14 +390,7 @@ public void filesDragged(List packs) { Path p = packs.get(i); futs[i] = CompletableFuture.runAsync(() -> { try { - var target = SKINS_DIR.resolve(p.getFileName()); - if (Files.exists(target)) { - int counter = 0; - do { - counter++; - target = target.resolveSibling(target.getFileName().toString() + "_" + counter); - } while (Files.exists(target)); - } + var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName())); var skin = Auth.getInstance().getSkinManager().read(p, false); if (skin != null) { Files.write(target, skin.image().join()); @@ -354,7 +401,7 @@ public void filesDragged(List packs) { } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); } - }, ThreadExecuter.service()); + }, client); } CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } @@ -547,6 +594,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { if (confirmed) { try { Files.delete(asset.file()); + Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); @@ -683,6 +731,8 @@ private float applyEasing(float x) { protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { int y = getY() + 4; int x = getX() + 2; + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); if (skinWidget.isEquipped() || equipping) { long prog; if (Auth.getInstance().skinManagerAnimations.get()) { @@ -701,8 +751,6 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float gradientWidth, equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); } - skinWidget.setPosition(x, y); - skinWidget.setWidth(getWidth() - 4); skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); int actionButtonY = getY() + 2; for (var button : actionButtons) { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 9b151fa39..556be0d3c 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -36,13 +36,14 @@ import com.mojang.blaze3d.vertex.VertexConsumer; import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.api.SimpleTextInputScreen; +import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.mixin.GameRendererAccessor; import io.github.axolotlclient.mixin.GuiGraphicsAccessor; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; -import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; @@ -139,7 +140,7 @@ protected void init() { skinList.visible = skinList.active = true; capesList.visible = capesList.active = false; capesTab = false; - }).pos(width * 3 / 4 - 102, headerHeight).width(100).build(); + }).pos(Math.max(width * 3 / 4 - 102, width / 2 + 2), headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(skinsTab); var capesTab = Button.builder(Component.translatable("skins.nav.capes"), btn -> { navBar.forEach(w -> { @@ -149,20 +150,29 @@ protected void init() { skinList.visible = skinList.active = false; capesList.visible = capesList.active = true; this.capesTab = true; - }).pos(width * 3 / 4 + 2, headerHeight).width(100).build(); + }).pos(width * 3 / 4 + 2, headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(capesTab); var importButton = SpriteIconButton.builder(Component.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::onFilesDrop).thenRun(() -> btn.active = true); }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "folder"), 7, 7).size(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); - importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); var downloadButton = SpriteIconButton.builder(Component.translatable("skins.manage.import.online"), btn -> { btn.active = false; - // TODO + promptForSkinDownload(); }, true).sprite(ResourceLocation.fromNamespaceAndPath("axolotlclient", "download"), 7, 7).size(11, 11).build(); downloadButton.setTooltip(Tooltip.create(downloadButton.getMessage())); - downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); + if (width - (capesTab.getX() + capesTab.getWidth()) > 28) { + importButton.setX(width - importButton.getWidth() - 2); + downloadButton.setX(importButton.getX() - downloadButton.getWidth() - 2); + importButton.setY(capesTab.getY() + capesTab.getHeight() - 11); + downloadButton.setY(importButton.getY()); + } else { + importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); + importButton.setY(capesTab.getY() - 13); + downloadButton.setX(importButton.getX() - 2 - 11); + downloadButton.setY(importButton.getY()); + } skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -205,6 +215,39 @@ protected void init() { }); } + private void promptForSkinDownload() { + minecraft.setScreen(new SimpleTextInputScreen(this, Component.translatable("skins.manage.import.online"), Component.translatable("skins.manage.import.online.input"), s -> + UUIDHelper.ensureUuidOpt(s).thenAcceptAsync(o -> { + if (o.isPresent()) { + AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get()); + Auth.getInstance().getMsApi().getTextures(o.get()) + .exceptionally(th -> { + AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th); + return null; + }).thenAcceptAsync(t -> { + if (t == null) { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s); + return; + } + try { + var bytes = t.skin().join(); + var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Files.write(out, bytes); + minecraft.execute(this::loadSkinsList); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); + AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get()); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name()); + } + }); + } else { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s); + } + }))); + } + private void initDisplay() { loadSkinsList(); loadCapesList(); @@ -320,6 +363,17 @@ private void populateSkinList(List skins, int columns) { } } + private Path ensureNonexistent(Path p) { + if (Files.exists(p)) { + int counter = 0; + do { + counter++; + p = p.resolveSibling(p.getFileName().toString() + "_" + counter); + } while (Files.exists(p)); + } + return p; + } + @Override public void onFilesDrop(List packs) { if (packs.isEmpty()) return; @@ -329,14 +383,7 @@ public void onFilesDrop(List packs) { Path p = packs.get(i); futs[i] = CompletableFuture.runAsync(() -> { try { - var target = SKINS_DIR.resolve(p.getFileName()); - if (Files.exists(target)) { - int counter = 0; - do { - counter++; - target = target.resolveSibling(target.getFileName().toString() + "_" + counter); - } while (Files.exists(target)); - } + var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName())); var skin = Auth.getInstance().getSkinManager().read(p, false); if (skin != null) { Files.write(target, skin.image().join()); @@ -347,7 +394,7 @@ public void onFilesDrop(List packs) { } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); } - }, ThreadExecuter.service()); + }, minecraft); } CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } @@ -545,6 +592,7 @@ public void renderString(GuiGraphics guiGraphics, Font font, int color) { if (confirmed) { try { Files.delete(asset.file()); + Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); @@ -631,6 +679,8 @@ private float applyEasing(float x) { protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { int y = getY() + 4; int x = getX() + 2; + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); if (skinWidget.isEquipped() || equipping) { long prog; if (Auth.getInstance().skinManagerAnimations.get()) { @@ -649,8 +699,6 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo gradientWidth, equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0).submit(); } - skinWidget.setPosition(x, y); - skinWidget.setWidth(getWidth() - 4); skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); int actionButtonY = getY() + 2; for (var button : actionButtons) { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index fcad07f9d..f250515a7 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -34,11 +34,12 @@ import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors; +import io.github.axolotlclient.api.SimpleTextInputScreen; +import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.ClientColors; -import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; @@ -138,7 +139,7 @@ protected void init() { skinList.visible = skinList.active = true; capesList.visible = capesList.active = false; capesTab = false; - }).position(width * 3 / 4 - 102, headerHeight).width(100).build(); + }).position(Math.max(width * 3 / 4 - 102, width / 2 + 2), headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(skinsTab); var capesTab = ButtonWidget.builder(Text.translatable("skins.nav.capes"), btn -> { navBar.forEach(w -> { @@ -148,20 +149,29 @@ protected void init() { skinList.visible = skinList.active = false; capesList.visible = capesList.active = true; this.capesTab = true; - }).position(width * 3 / 4 + 2, headerHeight).width(100).build(); + }).position(width * 3 / 4 + 2, headerHeight).width(Math.min(100, width / 4 - 2)).build(); navBar.add(capesTab); var importButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import.local"), btn -> { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true); }, true).sprite(Identifier.of("axolotlclient", "folder"), 7, 7).dimensions(11, 11).build(); importButton.setTooltip(Tooltip.create(importButton.getMessage())); - importButton.setPosition(capesTab.getX() + capesTab.getWidth() - 11, capesTab.getY() - 13); var downloadButton = SpriteButtonWidget.builder(Text.translatable("skins.manage.import.online"), btn -> { btn.active = false; - // TODO + promptForSkinDownload(); }, true).sprite(Identifier.of("axolotlclient", "download"), 7, 7).dimensions(11, 11).build(); downloadButton.setTooltip(Tooltip.create(downloadButton.getMessage())); - downloadButton.setPosition(importButton.getX() - 2 - 11, capesTab.getY() - 13); + if (width - (capesTab.getX() + capesTab.getWidth()) > 28) { + importButton.setX(width - importButton.getWidth() - 2); + downloadButton.setX(importButton.getX() - downloadButton.getWidth() - 2); + importButton.setY(capesTab.getY() + capesTab.getHeight() - 11); + downloadButton.setY(importButton.getY()); + } else { + importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); + importButton.setY(capesTab.getY() - 13); + downloadButton.setX(importButton.getX() - 2 - 11); + downloadButton.setY(importButton.getY()); + } skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -203,6 +213,39 @@ protected void init() { }); } + private void promptForSkinDownload() { + client.setScreen(new SimpleTextInputScreen(this, Text.translatable("skins.manage.import.online"), Text.translatable("skins.manage.import.online.input"), s -> + UUIDHelper.ensureUuidOpt(s).thenAccept(o -> { + if (o.isPresent()) { + AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get()); + Auth.getInstance().getMsApi().getTextures(o.get()) + .exceptionally(th -> { + AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th); + return null; + }).thenAccept(t -> { + if (t == null) { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s); + return; + } + try { + var bytes = t.skin().join(); + var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Files.write(out, bytes); + client.execute(this::loadSkinsList); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); + AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get()); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name()); + } + }); + } else { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s); + } + }))); + } + private void initDisplay() { loadSkinsList(); loadCapesList(); @@ -318,6 +361,17 @@ private void populateSkinList(List skins, int columns) { } } + private Path ensureNonexistent(Path p) { + if (Files.exists(p)) { + int counter = 0; + do { + counter++; + p = p.resolveSibling(p.getFileName().toString() + "_" + counter); + } while (Files.exists(p)); + } + return p; + } + @Override public void filesDragged(List packs) { if (packs.isEmpty()) return; @@ -327,14 +381,7 @@ public void filesDragged(List packs) { Path p = packs.get(i); futs[i] = CompletableFuture.runAsync(() -> { try { - var target = SKINS_DIR.resolve(p.getFileName()); - if (Files.exists(target)) { - int counter = 0; - do { - counter++; - target = target.resolveSibling(target.getFileName().toString() + "_" + counter); - } while (Files.exists(target)); - } + var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName())); var skin = Auth.getInstance().getSkinManager().read(p, false); if (skin != null) { Files.write(target, skin.image().join()); @@ -345,7 +392,7 @@ public void filesDragged(List packs) { } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); } - }, ThreadExecuter.service()); + }, client); } CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } @@ -552,6 +599,7 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int if (confirmed) { try { Files.delete(asset.file()); + Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); @@ -628,6 +676,8 @@ private float applyEasing(float x) { protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { int y = getY() + 4; int x = getX() + 2; + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); if (skinWidget.isEquipped() || equipping) { long prog; if (Auth.getInstance().skinManagerAnimations.get()) { @@ -646,8 +696,6 @@ protected void drawWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float gradientWidth, equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); } - skinWidget.setPosition(x, y); - skinWidget.setWidth(getWidth() - 4); skinWidget.render(guiGraphics, mouseX, mouseY, partialTick); int actionButtonY = getY() + 2; for (var button : actionButtons) { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/bridge/mixin/util/StyleMixin.java b/1.8.9/src/main/java/io/github/axolotlclient/bridge/mixin/util/StyleMixin.java index 61359dc51..78bc9451b 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/bridge/mixin/util/StyleMixin.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/bridge/mixin/util/StyleMixin.java @@ -23,6 +23,8 @@ package io.github.axolotlclient.bridge.mixin.util; import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.sugar.Local; import io.github.axolotlclient.bridge.util.AxoText; import net.minecraft.text.Formatting; @@ -31,6 +33,7 @@ import net.minecraft.text.Text; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -56,6 +59,13 @@ public abstract class StyleMixin implements AxoText.Style { @Shadow private Style parent; + @Shadow + protected abstract Style getParent(); + + @Shadow + @Final + private static Style ROOT; + @Override public AxoText.Style br$color(AxoText.Color color) { return copy().setColor(switch (color) { @@ -85,9 +95,18 @@ public abstract class StyleMixin implements AxoText.Style { return copy; } + @Unique + private Integer axolotlclient$getColor() { + if (((Object)this) == ROOT) return null; + if (axolotlclient$color == null) { + return ((StyleMixin) (Object)getParent()).axolotlclient$getColor(); + } + return axolotlclient$color; + } + @ModifyReturnValue(method = "copy", at = @At("RETURN")) public Style copyColor(Style original) { - ((StyleMixin) (Object) original).axolotlclient$color = axolotlclient$color; + ((StyleMixin) (Object) original).axolotlclient$color = axolotlclient$getColor(); return original; } @@ -104,16 +123,15 @@ public Style deepCopyColor(Style original) { @Inject(method = "asString", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/Style;isBold()Z")) private void formatColorCode(CallbackInfoReturnable cir, @Local StringBuilder sb) { - Integer color = null; - StyleMixin s = this; - - while (s != null && color == null) { - color = s.axolotlclient$color; - s = (StyleMixin) (Object) s.parent; - } + Integer color = axolotlclient$getColor(); if (color != null) { sb.append("§#").append(StringUtils.leftPad(Integer.toUnsignedString(color & 0xffffff, 16), 6, "0")); } } + + @WrapMethod(method = "isEmpty") + private boolean isEmptyColor(Operation original) { + return original.call() && axolotlclient$color == null; + } } diff --git a/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java b/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java new file mode 100644 index 000000000..ad1ace962 --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfigVanillaButtonWidgetMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin; + +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ButtonWidget; +import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.widgets.VanillaButtonWidget; +import io.github.axolotlclient.modules.hud.util.DrawUtil; +import io.github.axolotlclient.util.ButtonWidgetTextures; +import net.minecraft.client.Minecraft; +import net.minecraft.resource.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(VanillaButtonWidget.class) +public abstract class ConfigVanillaButtonWidgetMixin extends ButtonWidget { + private ConfigVanillaButtonWidgetMixin(int x, int y, int width, int height, String message, PressAction action) { + super(x, y, width, height, message, action); + } + + @Redirect(method = "drawWidget", at = @At(value = "INVOKE", target = "Lio/github/axolotlclient/AxolotlClientConfig/impl/ui/vanilla/widgets/VanillaButtonWidget;drawTexture(IIIIII)V", ordinal = 0)) + private void drawTexture$1(VanillaButtonWidget instance, int x, int y, int u, int v, int width, int height) { + + } + + @Redirect(method = "drawWidget", at = @At(value = "INVOKE", target = "Lio/github/axolotlclient/AxolotlClientConfig/impl/ui/vanilla/widgets/VanillaButtonWidget;drawTexture(IIIIII)V", ordinal = 1)) + private void drawTexture$2$replaceWithNineSlice(VanillaButtonWidget instance, int x, int y, int u, int v, int width, int height) { + Identifier tex = ButtonWidgetTextures.get(active ? (this.hovered ? 2 : 1) : 0); + DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3)); + Minecraft.getInstance().getTextureManager().bind(WIDGETS_LOCATION); + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfirmScreenMixin.java b/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfirmScreenMixin.java new file mode 100644 index 000000000..6972079b0 --- /dev/null +++ b/1.8.9/src/main/java/io/github/axolotlclient/mixin/ConfirmScreenMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2025 moehreag & Contributors + * + * This file is part of AxolotlClient. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * For more information, see the LICENSE file. + */ + +package io.github.axolotlclient.mixin; + +import java.util.List; + +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.render.TextRenderUtils; +import net.minecraft.client.render.TextRenderer; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ConfirmScreen.class) +public class ConfirmScreenMixin { + + @Redirect(method = "init", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/TextRenderer;split(Ljava/lang/String;I)Ljava/util/List;")) + private List fixTextWrapFormatting(TextRenderer instance, String string, int i) { + return TextRenderUtils.wrapText(new LiteralText(string), i, instance, true, true).stream().map(Text::getFormattedString).toList(); + } +} diff --git a/1.8.9/src/main/java/io/github/axolotlclient/mixin/TextRenderUtilsMixin.java b/1.8.9/src/main/java/io/github/axolotlclient/mixin/TextRenderUtilsMixin.java index c5c151009..7a79e753f 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/mixin/TextRenderUtilsMixin.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/mixin/TextRenderUtilsMixin.java @@ -31,6 +31,7 @@ import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.ref.LocalRef; import io.github.axolotlclient.AxolotlClient; +import io.github.axolotlclient.AxolotlClientConfig.api.util.Color; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.render.TextRenderUtils; import net.minecraft.client.render.TextRenderer; @@ -44,7 +45,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(TextRenderUtils.class) -public class TextRenderUtilsMixin { +public abstract class TextRenderUtilsMixin { @Unique private static final Map formattingCodes; @@ -132,6 +133,7 @@ private static Text formatFromCodes(String formattedString) { List modifiers = new ArrayList<>(); Formatting color = null; + Integer br$color = null; for (int i = 0, length = arr.length; i < length; i++) { String s = arr[i]; if (s.isEmpty()) { @@ -141,28 +143,37 @@ private static Text formatFromCodes(String formattedString) { continue; } Formatting formatting = byCodeOfFirstChar(s); - if (formatting == null) { - text.append(s); - continue; - } - Text part; int pL = s.length(); - if (formatting.equals(Formatting.RESET)) { - modifiers.clear(); - color = null; - } else if (formatting.isModifier()) { - modifiers.add(formatting); + int formatLength = 1; + if (formatting == null) { + if (s.toLowerCase(Locale.ROOT).charAt(0) == '#') { + br$color = Color.parse(s.substring(0, 7)).toInt(); + formatLength = 7; + } else { + text.append(s); + continue; + } } else { - color = formatting; + if (formatting.equals(Formatting.RESET)) { + modifiers.clear(); + color = null; + } else if (formatting.isModifier()) { + modifiers.add(formatting); + } else { + color = formatting; + } } if (pL == 1) { continue; } - part = new LiteralText(s.substring(1)); + part = new LiteralText(s.substring(formatLength)); if (color != null) { - part.getStyle().setColor(color); + part.setStyle(part.getStyle().setColor(color)); + } + if (br$color != null) { + part.br$setStyle(part.getStyle().br$color(br$color)); } if (!modifiers.isEmpty()) { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 1ce79af57..e22df648e 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -44,6 +44,8 @@ import io.github.axolotlclient.AxolotlClientConfig.impl.ui.ParentElement; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.ElementListWidget; import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.widgets.VanillaButtonWidget; +import io.github.axolotlclient.api.SimpleTextInputScreen; +import io.github.axolotlclient.api.util.UUIDHelper; import io.github.axolotlclient.bridge.util.AxoText; import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.Auth; @@ -51,7 +53,6 @@ import io.github.axolotlclient.modules.hud.util.DrawUtil; import io.github.axolotlclient.util.ButtonWidgetTextures; import io.github.axolotlclient.util.ClientColors; -import io.github.axolotlclient.util.ThreadExecuter; import io.github.axolotlclient.util.Watcher; import io.github.axolotlclient.util.notifications.Notifications; import net.fabricmc.loader.api.FabricLoader; @@ -162,7 +163,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { skinList.visible = skinList.active = false; } List navBar = new ArrayList<>(); - var skinsTab = new VanillaButtonWidget(width * 3 / 4 - 102, headerHeight, 100, 20, I18n.translate("skins.nav.skins"), btn -> { + var skinsTab = new VanillaButtonWidget(Math.max(width * 3 / 4 - 102, width / 2 + 2), headerHeight, Math.min(100, width / 4 - 2), 20, I18n.translate("skins.nav.skins"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); @@ -172,7 +173,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { capesTab = false; }); navBar.add(skinsTab); - var capesTab = new VanillaButtonWidget(width * 3 / 4 + 2, headerHeight, 100, 20, I18n.translate("skins.nav.capes"), btn -> { + var capesTab = new VanillaButtonWidget(width * 3 / 4 + 2, headerHeight, Math.min(100, width / 4 - 2), 20, I18n.translate("skins.nav.capes"), btn -> { navBar.forEach(w -> { if (w != btn) w.active = true; }); @@ -186,14 +187,21 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { btn.active = false; SkinImportUtil.openImportSkinDialog().thenAccept(this::onFileDrop).thenRun(() -> btn.active = true); }, new Identifier("axolotlclient", "textures/gui/sprites/folder.png")); - importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); - importButton.setY(capesTab.getY() - 13); var downloadButton = new SpriteButton(I18n.translate("skins.manage.import.online"), btn -> { btn.active = false; - // TODO + promptForSkinDownload(); }, new Identifier("axolotlclient", "textures/gui/sprites/download.png")); - downloadButton.setX(importButton.getX() - 2 - 11); - downloadButton.setY(capesTab.getY() - 13); + if (width - (capesTab.getX() + capesTab.getWidth()) > 28) { + importButton.setX(width - importButton.getWidth() - 2); + downloadButton.setX(importButton.getX() - downloadButton.getWidth() - 2); + importButton.setY(capesTab.getY() + capesTab.getHeight() - 11); + downloadButton.setY(importButton.getY()); + } else { + importButton.setX(capesTab.getX() + capesTab.getWidth() - 11); + importButton.setY(capesTab.getY() - 13); + downloadButton.setX(importButton.getX() - 2 - 11); + downloadButton.setY(importButton.getY()); + } skinsTab.active = this.capesTab; capesTab.active = !this.capesTab; Runnable addWidgets = () -> { @@ -236,6 +244,39 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { }); } + private void promptForSkinDownload() { + minecraft.openScreen(new SimpleTextInputScreen(this, I18n.translate("skins.manage.import.online"), I18n.translate("skins.manage.import.online.input"), s -> + UUIDHelper.ensureUuidOpt(s).thenAccept(o -> { + if (o.isPresent()) { + AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get()); + Auth.getInstance().getMsApi().getTextures(o.get()) + .exceptionally(th -> { + AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th); + return null; + }).thenAccept(t -> { + if (t == null) { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s); + return; + } + try { + var bytes = t.skin().join(); + var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Files.write(out, bytes); + minecraft.submit(this::loadSkinsList); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); + AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get()); + } catch (IOException e) { + AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e); + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name()); + } + }); + } else { + Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s); + } + }))); + } + private void initDisplay() { loadSkinsList(); loadCapesList(); @@ -350,6 +391,17 @@ private void populateSkinList(List skins, int columns) { } } + private Path ensureNonexistent(Path p) { + if (Files.exists(p)) { + int counter = 0; + do { + counter++; + p = p.resolveSibling(p.getFileName().toString() + "_" + counter); + } while (Files.exists(p)); + } + return p; + } + @Override public void onFileDrop(List packs) { if (packs.isEmpty()) return; @@ -359,14 +411,7 @@ public void onFileDrop(List packs) { Path p = packs.get(i); futs[i] = CompletableFuture.runAsync(() -> { try { - var target = SKINS_DIR.resolve(p.getFileName()); - if (Files.exists(target)) { - int counter = 0; - do { - counter++; - target = target.resolveSibling(target.getFileName().toString() + "_" + counter); - } while (Files.exists(target)); - } + var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName())); var skin = Auth.getInstance().getSkinManager().read(p, false); if (skin != null) { Files.write(target, skin.image().join()); @@ -377,7 +422,7 @@ public void onFileDrop(List packs) { } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e); } - }, ThreadExecuter.service()); + }, minecraft); } CompletableFuture.allOf(futs).thenRun(this::loadSkinsList); } @@ -587,6 +632,7 @@ public Entry(int height, SkinWidget widget, @Nullable String label) { if (confirmed) { try { Files.delete(asset.file()); + Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); @@ -717,6 +763,8 @@ private float applyEasing(float x) { protected void drawWidget(int mouseX, int mouseY, float partialTick) { int y = getY() + 4; int x = getX() + 2; + skinWidget.setPosition(x, y); + skinWidget.setWidth(getWidth() - 4); if (skinWidget.isEquipped() || equipping) { long prog; if (Auth.getInstance().skinManagerAnimations.get()) { @@ -735,8 +783,6 @@ protected void drawWidget(int mouseX, int mouseY, float partialTick) { gradientWidth, equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0); } - skinWidget.setPosition(x, y); - skinWidget.setWidth(getWidth() - 4); skinWidget.render(mouseX, mouseY, partialTick); int actionButtonY = getY() + 2; for (var button : actionButtons) { @@ -744,9 +790,6 @@ protected void drawWidget(int mouseX, int mouseY, float partialTick) { if (isHovered() || button.isHovered()) { button.render(mouseX, mouseY, partialTick); } - if (button.isHovered()) { - tooltip = button.getMessage(); - } actionButtonY += button.getHeight() + 2; } if (label != null) { @@ -829,6 +872,7 @@ protected void drawWidget(int mouseX, int mouseY, float delta) { i = 0; } else if (hovered) { i = 2; + tooltip = getMessage(); } Identifier tex = ButtonWidgetTextures.get(i); diff --git a/1.8.9/src/main/resources/axolotlclient.mixins.json b/1.8.9/src/main/resources/axolotlclient.mixins.json index 53595af66..0e0b07819 100644 --- a/1.8.9/src/main/resources/axolotlclient.mixins.json +++ b/1.8.9/src/main/resources/axolotlclient.mixins.json @@ -15,6 +15,8 @@ "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", "ClientWorldMixin", + "ConfigVanillaButtonWidgetMixin", + "ConfirmScreenMixin", "ConnectScreenMixin", "ControlsOptionsScreenMixin", "DebugHudMixin", diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java index fa2a6fff8..666e2ee10 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/MSApi.java @@ -27,8 +27,10 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -39,6 +41,7 @@ import com.github.mizosoft.methanol.FormBodyPublisher; import com.github.mizosoft.methanol.MediaType; import com.github.mizosoft.methanol.MultipartBodyPublisher; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.github.axolotlclient.AxolotlClientCommon; import io.github.axolotlclient.modules.auth.skin.Cape; @@ -439,6 +442,45 @@ public CompletableFuture getProfile(Account account) { return getMCProfile(account.getAuthToken()).thenApply(this::extractProfile); } + public record SkinBundle(String name, String id, CompletableFuture skin, String skinKey, + boolean classicModel) { + } + + public CompletableFuture getTextures(String uuid) { + return requestJson(HttpRequest.newBuilder().GET() + .uri(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid)) + .build()).thenApply(json -> { + var name = json.get("name").getAsString(); + var id = json.get("id").getAsString(); + var properties = json.get("properties").getAsJsonArray(); + for (JsonElement e : properties) { + if (e.isJsonObject()) { + var obj = e.getAsJsonObject(); + if (obj.has("name") && "textures".equals(obj.get("name").getAsString())) { + var b64 = obj.get("value").getAsString(); + var props = GsonHelper.fromJson(new String(Base64.getDecoder().decode(b64), StandardCharsets.UTF_8)); + var textures = props.get("textures").getAsJsonObject(); + if (textures.has("SKIN")) { + var skinObj = textures.get("SKIN").getAsJsonObject(); + var skinUrl = skinObj.get("url").getAsString(); + var skin = client.sendAsync(HttpRequest.newBuilder().uri(URI.create(skinUrl)).GET().build(), HttpResponse.BodyHandlers.ofByteArray()) + .thenApply(HttpResponse::body); + var skinKey = skinUrl.substring(skinUrl.lastIndexOf("/") + 1); + var classicModel = true; + if (skinObj.has("metadata")) { + var metadata = skinObj.get("metadata").getAsJsonObject(); + var model = metadata.get("model").getAsString(); + classicModel = MCProfile.OnlineSkin.VARIANT_CLASSIC.toLowerCase(Locale.ROOT).equals(model.toLowerCase(Locale.ROOT)); + } + return new SkinBundle(name, id, skin, skinKey, classicModel); + } + } + } + } + return null; + }); + } + private MCProfile extractProfile(JsonObject profileJson) { if (profileJson.has("error") && "NOT_FOUND".equals(profileJson.get("error").getAsString())) { throw new IllegalStateException("profile not found"); diff --git a/common/src/main/resources/assets/axolotlclient/lang/en_us.json b/common/src/main/resources/assets/axolotlclient/lang/en_us.json index b1145cb0a..99cd105aa 100644 --- a/common/src/main/resources/assets/axolotlclient/lang/en_us.json +++ b/common/src/main/resources/assets/axolotlclient/lang/en_us.json @@ -833,5 +833,10 @@ "skins.notification.title": "Skin Management", "skins.notification.not_copied": "Skipped file %s because it does not seem like a valid skin file!", "skins.manage.variant.classic": "Use Classic (Wide) Variant", - "skins.manage.variant.slim": "Use Slim Variant" + "skins.manage.variant.slim": "Use Slim Variant", + "skins.manage.import.online.input": "Enter Name or UUID:", + "skins.notification.import.online.failed_to_download": "Failed to download Skin of %s!", + "skins.notification.import.online.downloaded": "Downloaded Skin of %s!", + "skins.notification.import.online.failed_to_save": "Failed to save Skin of %s!", + "skins.notification.import.online.not_found": "Could not find user %s!" } diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png index 84f7fb765d29c8941b67d6626d0e6ba8bf7665ae..3a48c808588f1f5a48055014088dd500a8c5185c 100644 GIT binary patch delta 92 zcmV-i0Hgog0p0HJJjCQCN$ y4G4uol3nxSgQf?7ZD~^4R?{@Yq`bXeBftT9au`7i4z=9?0000atIV!#8Ku<3yD v8L=7Z;NZZBVGOznm;y-RjJPBj7#J7;t)v>j!AOvw00000NkvXXu0mjf`dA_% From e184a131edf3b40951feb776993c7e17535f5495 Mon Sep 17 00:00:00 2001 From: moehreag Date: Mon, 8 Sep 2025 17:03:34 +0200 Subject: [PATCH 21/23] clean up a bit --- .../auth/skin/SkinManagementScreen.java | 11 +++--- .../modules/auth/skin/SkinManager.java | 1 - .../api/SimpleTextInputScreen.java | 5 +++ .../auth/skin/SkinManagementScreen.java | 11 +++--- .../modules/auth/skin/SkinManager.java | 1 - .../auth/skin/SkinManagementScreen.java | 11 +++--- .../modules/auth/skin/SkinManager.java | 1 - .../auth/skin/SkinManagementScreen.java | 11 +++--- .../modules/auth/skin/SkinManager.java | 1 - .../auth/skin/SkinManagementScreen.java | 7 ++-- .../modules/auth/skin/SkinManager.java | 1 - .../axolotlclient/modules/auth/skin/Skin.java | 35 +++++++++++------- .../io/github/axolotlclient/util/Watcher.java | 8 ---- .../textures/gui/sprites/download.png | Bin 222 -> 217 bytes .../textures/gui/sprites/folder.png | Bin 217 -> 217 bytes .../textures/gui/sprites/slim.png | Bin 215 -> 215 bytes .../textures/gui/sprites/wide.png | Bin 217 -> 213 bytes 17 files changed, 54 insertions(+), 50 deletions(-) diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index f08d7817c..045ba579d 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -90,7 +91,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(new TranslatableText("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -193,12 +194,12 @@ protected MutableText getNarrationMessage() { Runnable addWidgets = () -> { clear(); addDrawableChild(current); - addDrawableChild(skinList); - addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); addDrawableChild(downloadButton); addDrawableChild(importButton); + addDrawableChild(skinList); + addDrawableChild(capesList); addDrawableChild(back); }; if (cachedProfile != null) { @@ -256,7 +257,7 @@ private void promptForSkinDownload() { try { var bytes = t.skin().join(); var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); - Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id(), "download_time", Instant.now())); Files.write(out, bytes); client.execute(this::loadSkinsList); Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); @@ -626,7 +627,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { if (confirmed) { try { Files.delete(asset.file()); - Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); + Skin.Local.deleteMetadata(asset.file()); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); diff --git a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index d2ef6265e..24b2871e8 100644 --- a/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -56,7 +56,6 @@ public Skin read(Path p) { @SuppressWarnings("UnstableApiUsage") public Skin read(Path p, boolean fix) { - if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { diff --git a/1.20/src/main/java/io/github/axolotlclient/api/SimpleTextInputScreen.java b/1.20/src/main/java/io/github/axolotlclient/api/SimpleTextInputScreen.java index ebf2034f6..dccffcfb3 100644 --- a/1.20/src/main/java/io/github/axolotlclient/api/SimpleTextInputScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/api/SimpleTextInputScreen.java @@ -59,6 +59,11 @@ public void init() { }).positionAndSize(width / 2 + 5, height - 50, 150, 20).build()); } + @Override + public void tick() { + input.tick(); + } + @Override public void render(GuiGraphics graphics, int i, int j, float f) { renderBackground(graphics); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 95e407a82..e820771f0 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -80,7 +81,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -182,12 +183,12 @@ protected void updateNarration(NarrationMessageBuilder builder) { clearChildren(); addDrawableChild(titleWidget); addDrawableChild(current); - addDrawableChild(skinList); - addDrawableChild(capesList); addDrawableChild(skinsTab); addDrawableChild(capesTab); addDrawableChild(downloadButton); addDrawableChild(importButton); + addDrawableChild(skinList); + addDrawableChild(capesList); addDrawableChild(back); }; if (cachedProfile != null) { @@ -234,7 +235,7 @@ private void promptForSkinDownload() { try { var bytes = t.skin().join(); var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); - Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id(), "download_time", Instant.now())); Files.write(out, bytes); client.execute(this::loadSkinsList); Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); @@ -594,7 +595,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) { if (confirmed) { try { Files.delete(asset.file()); - Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); + Skin.Local.deleteMetadata(asset.file()); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); diff --git a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index 41af7f223..e7e48943c 100644 --- a/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.20/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -53,7 +53,6 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { - if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index 556be0d3c..b9c3c6848 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -89,7 +90,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Component.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -181,10 +182,10 @@ protected void init() { addRenderableWidget(current); addRenderableWidget(skinsTab); addRenderableWidget(capesTab); - addRenderableWidget(skinList); - addRenderableWidget(capesList); addRenderableWidget(downloadButton); addRenderableWidget(importButton); + addRenderableWidget(skinList); + addRenderableWidget(capesList); addRenderableWidget(back); }; if (cachedProfile != null) { @@ -232,7 +233,7 @@ private void promptForSkinDownload() { try { var bytes = t.skin().join(); var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); - Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id(), "download_time", Instant.now())); Files.write(out, bytes); minecraft.execute(this::loadSkinsList); Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); @@ -592,7 +593,7 @@ public void renderString(GuiGraphics guiGraphics, Font font, int color) { if (confirmed) { try { Files.delete(asset.file()); - Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); + Skin.Local.deleteMetadata(asset.file()); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index e748da430..4fdaa5d13 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -54,7 +54,6 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { - if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index f250515a7..ef10579b9 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -89,7 +90,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(Text.translatable("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -180,10 +181,10 @@ protected void init() { addDrawableSelectableElement(current); addDrawableSelectableElement(skinsTab); addDrawableSelectableElement(capesTab); - addDrawableSelectableElement(skinList); - addDrawableSelectableElement(capesList); addDrawableSelectableElement(downloadButton); addDrawableSelectableElement(importButton); + addDrawableSelectableElement(skinList); + addDrawableSelectableElement(capesList); addDrawableSelectableElement(back); }; if (cachedProfile != null) { @@ -230,7 +231,7 @@ private void promptForSkinDownload() { try { var bytes = t.skin().join(); var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); - Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id(), "download_time", Instant.now())); Files.write(out, bytes); client.execute(this::loadSkinsList); Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); @@ -599,7 +600,7 @@ public void drawScrollableText(GuiGraphics graphics, TextRenderer renderer, int if (confirmed) { try { Files.delete(asset.file()); - Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); + Skin.Local.deleteMetadata(asset.file()); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); diff --git a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index afb272c82..aaa7d678b 100644 --- a/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.21/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -53,7 +53,6 @@ public Skin read(Path p) { } public Skin read(Path p, boolean fix) { - if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java index e22df648e..3e7c829f6 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -86,7 +87,7 @@ public SkinManagementScreen(Screen parent, Account account) { super(I18n.translate("skins.manage")); this.parent = parent; this.account = account; - skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, s -> !s.endsWith(Skin.Local.METADATA_SUFFIX), () -> { + skinDirWatcher = Watcher.createSelfTicking(SKINS_DIR, () -> { AxolotlClientCommon.getInstance().getLogger().info("Reloading screen as local files changed!"); loadSkinsList(); }); @@ -261,7 +262,7 @@ private void promptForSkinDownload() { try { var bytes = t.skin().join(); var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey())); - Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id())); + Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id(), "download_time", Instant.now())); Files.write(out, bytes); minecraft.submit(this::loadSkinsList); Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name()); @@ -632,7 +633,7 @@ public Entry(int height, SkinWidget widget, @Nullable String label) { if (confirmed) { try { Files.delete(asset.file()); - Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX)); + Skin.Local.deleteMetadata(asset.file()); refreshCurrentList(); } catch (IOException e) { AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e); diff --git a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java index e541ec320..c328bee1c 100644 --- a/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java +++ b/1.8.9/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java @@ -56,7 +56,6 @@ public Skin read(Path p) { @SuppressWarnings("UnstableApiUsage") public Skin read(Path p, boolean fix) { - if (p.getFileName().toString().endsWith(Skin.Local.METADATA_SUFFIX)) return null; boolean slim; String sha256; try { diff --git a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java index 6f4a998b3..a57237bc6 100644 --- a/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java +++ b/common/src/main/java/io/github/axolotlclient/modules/auth/skin/Skin.java @@ -34,6 +34,7 @@ import io.github.axolotlclient.modules.auth.Account; import io.github.axolotlclient.modules.auth.MSApi; import io.github.axolotlclient.util.GsonHelper; +import org.jetbrains.annotations.NotNull; public interface Skin extends Asset { boolean classicVariant(); @@ -41,6 +42,7 @@ public interface Skin extends Asset { void classicVariant(boolean classic); final class Local implements Skin { + public static final String META_DIR = ".meta"; public static final String METADATA_SUFFIX = ".meta"; public static final String CLASSIC_METADATA_KEY = "variant_classic"; private boolean classic; @@ -54,28 +56,30 @@ public Local(boolean classic, Path file, String textureKey) { } @SuppressWarnings("unchecked") - public static Map readMetadata(Path skinFile) { - var metadataFile = skinFile.resolveSibling(skinFile.getFileName().toString() + METADATA_SUFFIX); + public static Map readMetadata(Path skinFile) throws IOException { + var metadataFile = getMetadataFile(skinFile); if (!Files.exists(metadataFile)) return null; try (var in = Files.newInputStream(metadataFile)) { return (Map) GsonHelper.read(in); - } catch (IOException ignored) { - } - return null; } - public static void writeMetadata(Path skinFile, Object metadata) { - var metadataFile = skinFile.resolveSibling(skinFile.getFileName().toString() + METADATA_SUFFIX); - try (var out = Files.newOutputStream(metadataFile); + public static void writeMetadata(Path skinFile, Map metadata) throws IOException { + try (var out = Files.newOutputStream(getMetadataFile(skinFile)); var writer = new OutputStreamWriter(out)) { GsonHelper.GSON.toJson(metadata, writer); - } catch (IOException ignored) { - } } + public static void deleteMetadata(Path skinFile) throws IOException { + Files.deleteIfExists(getMetadataFile(skinFile)); + } + + private static @NotNull Path getMetadataFile(Path skinFile) { + return skinFile.resolveSibling(META_DIR).resolve(skinFile.getFileName().toString() + METADATA_SUFFIX); + } + @Override public boolean classicVariant() { return classic; @@ -84,10 +88,13 @@ public boolean classicVariant() { @Override public void classicVariant(boolean classic) { if (classic != this.classic) { - var metadata = readMetadata(file()); - if (metadata == null) metadata = new HashMap<>(); - metadata.put(CLASSIC_METADATA_KEY, classic); - writeMetadata(file(), metadata); + try { + var metadata = readMetadata(file()); + if (metadata == null) metadata = new HashMap<>(); + metadata.put(CLASSIC_METADATA_KEY, classic); + writeMetadata(file(), metadata); + } catch (IOException ignored) { + } } this.classic = classic; } diff --git a/common/src/main/java/io/github/axolotlclient/util/Watcher.java b/common/src/main/java/io/github/axolotlclient/util/Watcher.java index 5cedec876..fd450696e 100644 --- a/common/src/main/java/io/github/axolotlclient/util/Watcher.java +++ b/common/src/main/java/io/github/axolotlclient/util/Watcher.java @@ -46,14 +46,6 @@ public Watcher(Path root, Predicate fileFilter) throws IOException { try { this.watchDir(path); - try (DirectoryStream directoryStream = Files.newDirectoryStream(path)) { - - for (Path path : directoryStream) { - if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { - this.watchDir(path); - } - } - } } catch (Exception e) { this.watcher.close(); throw e; diff --git a/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png b/common/src/main/resources/assets/axolotlclient/textures/gui/sprites/download.png index 3a48c808588f1f5a48055014088dd500a8c5185c..f425b247133b59e825b2a93bc7ab98864299407f 100644 GIT binary patch delta 85 zcmV-b0IL7q0oehNi%t#*EXAZ@l>h($FG)l}R0!97jj;^?Fbu-%{{J(bDrF#L1B8L0 rB$@v3k!=B>YD3O^{jA8ByOjVJr*<3i0L!cJ00000NkvXXu0mjf$i^dL delta 90 zcmV-g0Hy!g0p003SXK24wQ}!vFvP07*qoM6N<$f;;#hQUCw| delta 86 zcmcb~c$0BLmp{9pmX0p}<#h}U4Emlfjv*Y^lXuIXQ%PQ%MIt!kEAIn1A*S` l2g?$SC0DB@-LdgtV36CQf5YnRncqN-44$rjF6*2UngA=LAJ+f? delta 84 zcmV-a0IUDk0oMVLicJR)6dTpG2kQU;04+&GK~xyiV_+Cu;J|?c{|SjR;nRT5XT)Zr qgM$MjE}P({U^5x3CSu$?NB{tK)+f2NM|)EM0000fMw76lLN?a~uY2tI!1$iz@~O?~HlzrF^bRt8U3KbLh*2~7YqkRG-G From 5d4fd0f6c56e94dd6ad16b3022d1dd0ac8faec01 Mon Sep 17 00:00:00 2001 From: moehreag Date: Mon, 8 Sep 2025 18:17:01 +0200 Subject: [PATCH 22/23] [skip] update changelog --- CHANGELOG.md | 1 + build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70208d620..345d840ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 3.1.6 - Implement more options for the Inventory HUD (#173) +- Add Skin Manager (#176) ### 3.1.5 diff --git a/build.gradle.kts b/build.gradle.kts index 742c79f98..3886eedb7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -92,7 +92,7 @@ subprojects { return@forEach } val oldName = old.fileName.toString() - val oldVer = oldName.substring(0, oldName.indexOf("+")) + val oldVer = oldName.substringBefore("+") val mcVer = oldName.substring(oldName.indexOf("+") + 1, oldName.length - 4).removeSuffix("-sources") if (!project.version.toString().contains(mcVer)) { return@forEach From dd4207c583eb29d0b8f33bb0879604e8ab3291aa Mon Sep 17 00:00:00 2001 From: moehreag Date: Tue, 16 Sep 2025 18:24:53 +0200 Subject: [PATCH 23/23] tiny change --- .../github/axolotlclient/modules/auth/skin/SkinWidget.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java index 1ec8f110d..6cece9f9d 100644 --- a/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java +++ b/1.21.7/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java @@ -124,11 +124,6 @@ public void playDownSound(SoundManager handler) { protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) { } - @Override - public boolean isActive() { - return false; - } - @Nullable @Override public ComponentPath nextFocusPath(FocusNavigationEvent event) {