Skip to content
This repository was archived by the owner on Nov 28, 2025. It is now read-only.

Commit dad4c91

Browse files
committed
implement Skin downloads
- fix some bugs
1 parent ea2d30a commit dad4c91

File tree

18 files changed

+603
-143
lines changed

18 files changed

+603
-143
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright © 2025 moehreag <[email protected]> & Contributors
3+
*
4+
* This file is part of AxolotlClient.
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*
20+
* For more information, see the LICENSE file.
21+
*/
22+
23+
package io.github.axolotlclient.mixin;
24+
25+
import io.github.axolotlclient.AxolotlClientConfig.impl.ui.vanilla.widgets.VanillaButtonWidget;
26+
import io.github.axolotlclient.modules.hud.util.DrawUtil;
27+
import io.github.axolotlclient.util.ButtonWidgetTextures;
28+
import net.minecraft.client.MinecraftClient;
29+
import net.minecraft.client.gui.widget.ButtonWidget;
30+
import net.minecraft.client.util.math.MatrixStack;
31+
import net.minecraft.text.Text;
32+
import net.minecraft.util.Identifier;
33+
import org.spongepowered.asm.mixin.Mixin;
34+
import org.spongepowered.asm.mixin.Shadow;
35+
import org.spongepowered.asm.mixin.injection.At;
36+
import org.spongepowered.asm.mixin.injection.Redirect;
37+
38+
@Mixin(VanillaButtonWidget.class)
39+
public abstract class ConfigVanillaButtonWidgetMixin extends ButtonWidget {
40+
@Shadow(remap = false)
41+
public abstract int getX();
42+
43+
@Shadow(remap = false)
44+
public abstract int getY();
45+
46+
private ConfigVanillaButtonWidgetMixin(int x, int y, int width, int height, Text message, PressAction action) {
47+
super(x, y, width, height, message, action);
48+
}
49+
50+
@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))
51+
private void drawTexture$1(VanillaButtonWidget instance, MatrixStack stack, int x, int y, int u, int v, int width, int height) {
52+
53+
}
54+
55+
@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))
56+
private void drawTexture$2$replaceWithNineSlice(VanillaButtonWidget instance, MatrixStack stack, int x, int y, int u, int v, int width, int height) {
57+
Identifier tex = ButtonWidgetTextures.get(getYImage(hovered));
58+
DrawUtil.blitSprite(tex, getX(), getY(), getWidth(), getHeight(), new DrawUtil.NineSlice(200, 20, 3));
59+
MinecraftClient.getInstance().getTextureManager().bindTexture(WIDGETS_LOCATION);
60+
}
61+
}

1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManagementScreen.java

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@
3535
import com.mojang.blaze3d.systems.RenderSystem;
3636
import io.github.axolotlclient.AxolotlClientCommon;
3737
import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors;
38+
import io.github.axolotlclient.api.SimpleTextInputScreen;
39+
import io.github.axolotlclient.api.util.UUIDHelper;
3840
import io.github.axolotlclient.modules.auth.Account;
3941
import io.github.axolotlclient.modules.auth.Auth;
4042
import io.github.axolotlclient.modules.auth.MSApi;
4143
import io.github.axolotlclient.modules.hud.util.DrawUtil;
4244
import io.github.axolotlclient.util.ButtonWidgetTextures;
4345
import io.github.axolotlclient.util.ClientColors;
44-
import io.github.axolotlclient.util.ThreadExecuter;
4546
import io.github.axolotlclient.util.Watcher;
4647
import io.github.axolotlclient.util.notifications.Notifications;
4748
import net.fabricmc.loader.api.FabricLoader;
@@ -146,7 +147,7 @@ protected MutableText getNarrationMessage() {
146147
skinList.visible = skinList.active = false;
147148
}
148149
List<AbstractButtonWidget> navBar = new ArrayList<>();
149-
var skinsTab = new ButtonWidget(width * 3 / 4 - 102, headerHeight, 100, 20, new TranslatableText("skins.nav.skins"), btn -> {
150+
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 -> {
150151
navBar.forEach(w -> {
151152
if (w != btn) w.active = true;
152153
});
@@ -156,7 +157,7 @@ protected MutableText getNarrationMessage() {
156157
capesTab = false;
157158
});
158159
navBar.add(skinsTab);
159-
var capesTab = new ButtonWidget(width * 3 / 4 + 2, headerHeight, 100, 20, new TranslatableText("skins.nav.capes"), btn -> {
160+
var capesTab = new ButtonWidget(width * 3 / 4 + 2, headerHeight, Math.min(100, width / 4 - 2), 20, new TranslatableText("skins.nav.capes"), btn -> {
160161
navBar.forEach(w -> {
161162
if (w != btn) w.active = true;
162163
});
@@ -170,14 +171,23 @@ protected MutableText getNarrationMessage() {
170171
btn.active = false;
171172
SkinImportUtil.openImportSkinDialog().thenAccept(this::filesDragged).thenRun(() -> btn.active = true);
172173
}, new Identifier("axolotlclient", "textures/gui/sprites/folder.png"));
173-
importButton.x = capesTab.x + capesTab.getWidth() - 11;
174-
importButton.y = capesTab.y - 13;
175174
var downloadButton = new SpriteButton(new TranslatableText("skins.manage.import.online"), btn -> {
176175
btn.active = false;
177-
// TODO
176+
promptForSkinDownload();
178177
}, new Identifier("axolotlclient", "textures/gui/sprites/download.png"));
179178
downloadButton.x = importButton.x - 2 - 11;
180179
downloadButton.y = capesTab.y - 13;
180+
if (width - (capesTab.x + capesTab.getWidth()) > 28) {
181+
importButton.x = width - importButton.getWidth() - 2;
182+
downloadButton.x = importButton.x - downloadButton.getWidth() - 2;
183+
importButton.y = capesTab.y + capesTab.getHeight() - 11;
184+
downloadButton.y = importButton.y;
185+
} else {
186+
importButton.x = capesTab.x + capesTab.getWidth() - 11;
187+
importButton.y = capesTab.y - 13;
188+
downloadButton.x = importButton.x - 2 - 11;
189+
downloadButton.y = importButton.y;
190+
}
181191
skinsTab.active = this.capesTab;
182192
capesTab.active = !this.capesTab;
183193
Runnable addWidgets = () -> {
@@ -229,6 +239,39 @@ public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float del
229239
});
230240
}
231241

242+
private void promptForSkinDownload() {
243+
client.openScreen(new SimpleTextInputScreen(this, new TranslatableText("skins.manage.import.online"), new TranslatableText("skins.manage.import.online.input"), s ->
244+
UUIDHelper.ensureUuidOpt(s).thenAccept(o -> {
245+
if (o.isPresent()) {
246+
AxolotlClientCommon.getInstance().getLogger().info("Downloading skin of {} ({})", s, o.get());
247+
Auth.getInstance().getMsApi().getTextures(o.get())
248+
.exceptionally(th -> {
249+
AxolotlClientCommon.getInstance().getLogger().info("Failed to download skin of {} ({})", s, o.get(), th);
250+
return null;
251+
}).thenAccept(t -> {
252+
if (t == null) {
253+
Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_download", s);
254+
return;
255+
}
256+
try {
257+
var bytes = t.skin().join();
258+
var out = ensureNonexistent(SKINS_DIR.resolve(t.skinKey()));
259+
Skin.Local.writeMetadata(out, Map.of(Skin.Local.CLASSIC_METADATA_KEY, t.classicModel(), "name", t.name(), "uuid", t.id()));
260+
Files.write(out, bytes);
261+
client.execute(this::loadSkinsList);
262+
Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.downloaded", t.name());
263+
AxolotlClientCommon.getInstance().getLogger().info("Downloaded skin of {} ({})", t.name(), o.get());
264+
} catch (IOException e) {
265+
AxolotlClientCommon.getInstance().getLogger().warn("Failed to write skin file", e);
266+
Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.failed_to_save", t.name());
267+
}
268+
});
269+
} else {
270+
Notifications.getInstance().addStatus("skins.notification.title", "skins.notification.import.online.not_found", s);
271+
}
272+
})));
273+
}
274+
232275
private <T extends Drawable & Element> T addDrawableChild(T child) {
233276
drawables.add(child);
234277
return addChild(child);
@@ -365,6 +408,17 @@ private void populateSkinList(List<? extends Skin> skins, int columns) {
365408
}
366409
}
367410

411+
private Path ensureNonexistent(Path p) {
412+
if (Files.exists(p)) {
413+
int counter = 0;
414+
do {
415+
counter++;
416+
p = p.resolveSibling(p.getFileName().toString() + "_" + counter);
417+
} while (Files.exists(p));
418+
}
419+
return p;
420+
}
421+
368422
@Override
369423
public void filesDragged(List<Path> packs) {
370424
if (packs.isEmpty()) return;
@@ -374,14 +428,7 @@ public void filesDragged(List<Path> packs) {
374428
Path p = packs.get(i);
375429
futs[i] = CompletableFuture.runAsync(() -> {
376430
try {
377-
var target = SKINS_DIR.resolve(p.getFileName());
378-
if (Files.exists(target)) {
379-
int counter = 0;
380-
do {
381-
counter++;
382-
target = target.resolveSibling(target.getFileName().toString() + "_" + counter);
383-
} while (Files.exists(target));
384-
}
431+
var target = ensureNonexistent(SKINS_DIR.resolve(p.getFileName()));
385432
var skin = Auth.getInstance().getSkinManager().read(p, false);
386433
if (skin != null) {
387434
Files.write(target, skin.image().join());
@@ -392,7 +439,7 @@ public void filesDragged(List<Path> packs) {
392439
} catch (IOException e) {
393440
AxolotlClientCommon.getInstance().getLogger().warn("Failed to copy skin file: ", e);
394441
}
395-
}, ThreadExecuter.service());
442+
}, client);
396443
}
397444
CompletableFuture.allOf(futs).thenRun(this::loadSkinsList);
398445
}
@@ -579,6 +626,7 @@ public Entry(int height, SkinWidget widget, @Nullable Text label) {
579626
if (confirmed) {
580627
try {
581628
Files.delete(asset.file());
629+
Files.deleteIfExists(asset.file().resolveSibling(asset.file().getFileName() + Skin.Local.METADATA_SUFFIX));
582630
refreshCurrentList();
583631
} catch (IOException e) {
584632
AxolotlClientCommon.getInstance().getLogger().warn("Failed to delete: ", e);
@@ -701,6 +749,8 @@ private float applyEasing(float x) {
701749
public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) {
702750
int y = this.y + 4;
703751
int x = this.x + 2;
752+
skinWidget.setPosition(x, y);
753+
skinWidget.setWidth(getWidth() - 4);
704754
if (skinWidget.isEquipped() || equipping) {
705755
long prog;
706756
if (Auth.getInstance().skinManagerAnimations.get()) {
@@ -719,8 +769,6 @@ public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float
719769
gradientWidth,
720770
equipping ? 0xFFFF0088 : ClientColors.SELECTOR_GREEN.toInt(), 0);
721771
}
722-
skinWidget.setPosition(x, y);
723-
skinWidget.setWidth(getWidth() - 4);
724772
skinWidget.render(guiGraphics, mouseX, mouseY, partialTick);
725773
int actionButtonY = this.y + 2;
726774
for (var button : actionButtons) {

1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinManager.java

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222

2323
package io.github.axolotlclient.modules.auth.skin;
2424

25+
import java.io.ByteArrayInputStream;
2526
import java.io.IOException;
2627
import java.io.UncheckedIOException;
27-
import java.nio.ByteBuffer;
2828
import java.nio.file.Files;
2929
import java.nio.file.Path;
3030
import java.util.Comparator;
@@ -45,7 +45,6 @@
4545
import net.minecraft.client.texture.NativeImageBackedTexture;
4646
import net.minecraft.client.util.DefaultSkinHelper;
4747
import net.minecraft.util.Identifier;
48-
import org.lwjgl.system.MemoryStack;
4948

5049
public class SkinManager {
5150

@@ -63,11 +62,8 @@ public Skin read(Path p, boolean fix) {
6362
try {
6463
var in = Files.readAllBytes(p);
6564
sha256 = Hashing.sha256().hashBytes(in).toString();
66-
try (MemoryStack memoryStack = MemoryStack.stackPush()) {
67-
ByteBuffer byteBuffer = memoryStack.malloc(in.length);
68-
byteBuffer.put(in);
69-
byteBuffer.rewind();
70-
try (var img = NativeImage.read(byteBuffer)) {
65+
try (var stream = new ByteArrayInputStream(in)) {
66+
try (var img = NativeImage.read(stream)) {
7167
int width = img.getWidth();
7268
int height = img.getHeight();
7369
if (width != 64) return null;
@@ -103,11 +99,9 @@ public CompletableFuture<AxoIdentifier> loadSkin(Skin skin) {
10399
}
104100

105101
return skin.image().thenApplyAsync(bytes -> {
106-
try (MemoryStack memoryStack = MemoryStack.stackPush()) {
107-
ByteBuffer byteBuffer = memoryStack.malloc(bytes.length);
108-
byteBuffer.put(bytes);
109-
byteBuffer.rewind();
110-
var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer));
102+
try (var stream = new ByteArrayInputStream(bytes)) {
103+
var tex = new NativeImageBackedTexture(NativeImage.read(stream));
104+
tex.upload();
111105
MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex);
112106
} catch (IOException e) {
113107
throw new UncheckedIOException(e);
@@ -129,11 +123,8 @@ public AxoIdentifier loadCape(Cape cape) {
129123
}
130124

131125
return cape.image().thenApplyAsync(bytes -> {
132-
try (MemoryStack memoryStack = MemoryStack.stackPush()) {
133-
ByteBuffer byteBuffer = memoryStack.malloc(bytes.length);
134-
byteBuffer.put(bytes);
135-
byteBuffer.rewind();
136-
var tex = new NativeImageBackedTexture(NativeImage.read(byteBuffer));
126+
try (var stream = new ByteArrayInputStream(bytes)) {
127+
var tex = new NativeImageBackedTexture(NativeImage.read(stream));
137128
MinecraftClient.getInstance().getTextureManager().registerTexture((Identifier) rl, tex);
138129
} catch (IOException e) {
139130
throw new UncheckedIOException(e);

1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinRenderer.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,7 @@ public static void render(MatrixStack graphics, boolean classicVariant,
7777
var tessellator = Tessellator.getInstance();
7878
RenderSystem.enableDepthTest();
7979
RenderSystem.enableBlend();
80-
if (cape != null) {
81-
graphics.push();
82-
MinecraftClient.getInstance().getTextureManager().bindTexture(cape);
83-
graphics.translate(0.0F, 0.0F, 0.125F);
84-
graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F));
85-
graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F));
86-
model.renderCape(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV);
87-
tessellator.draw();
88-
graphics.pop();
89-
}
80+
RenderSystem.enableTexture();
9081
MinecraftClient.getInstance().getTextureManager().bindTexture(skinTexture);
9182
var consumer = VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(model.getLayer(skinTexture));
9283
Consumer<ModelPart> 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,
10596
graphics.translate(0, 0, 0.62f);
10697
renderModelPart.accept(model.jacket);
10798
tessellator.draw();
99+
if (cape != null) {
100+
graphics.push();
101+
MinecraftClient.getInstance().getTextureManager().bindTexture(cape);
102+
graphics.translate(0.0F, 0.0F, 0.125F);
103+
graphics.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(6.0F));
104+
graphics.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(180.0F));
105+
model.renderCape(graphics, VertexConsumerProvider.immediate(tessellator.getBuffer()).getBuffer(RenderLayer.getEntitySolid(cape)), 15728880, OverlayTexture.DEFAULT_UV);
106+
tessellator.draw();
107+
graphics.pop();
108+
}
108109
graphics.pop();
109110

110111
graphics.pop();

1.16_combat-6/src/main/java/io/github/axolotlclient/modules/auth/skin/SkinWidget.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import io.github.axolotlclient.modules.auth.MSApi;
3232
import lombok.Getter;
3333
import lombok.Setter;
34-
import net.minecraft.client.MinecraftClient;
3534
import net.minecraft.client.gui.widget.AbstractButtonWidget;
3635
import net.minecraft.client.sound.SoundManager;
3736
import net.minecraft.client.util.DefaultSkinHelper;
@@ -79,8 +78,6 @@ public void noCape(boolean noCapeActive) {
7978

8079
@Override
8180
public void renderButton(MatrixStack guiGraphics, int mouseX, int mouseY, float partialTick) {
82-
var minecraft = MinecraftClient.getInstance();
83-
8481
float scale = FIT_SCALE * this.getHeight() / MODEL_HEIGHT;
8582
float pivotY = -1.0625F;
8683

1.16_combat-6/src/main/resources/axolotlclient.mixins.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"ClientPlayerEntityMixin",
1818
"ClientPlayNetworkHandlerMixin",
1919
"ClientWorldMixin",
20+
"ConfigVanillaButtonWidgetMixin",
2021
"DebugHudMixin",
2122
"DownloadingTerrainScreenMixin",
2223
"EmitterParticleMixin",

0 commit comments

Comments
 (0)