From dc4edc8d96a0c1aa55a5968685f46660e3cc4f1b Mon Sep 17 00:00:00 2001 From: Hamish Date: Wed, 5 Nov 2025 13:49:51 +1100 Subject: [PATCH 01/10] Fabric 1.21.10 --- gradle.properties | 2 +- .../SkinKeybindManagerClient.java | 315 ++++++++---------- .../SkinKeybindManagerScreen.java | 190 +++++++++++ .../name/modid/SkinKeybindManagerScreen.java | 144 -------- .../theduckman64}/SkinKeybindManager.java | 2 +- .../theduckman64}/mixin/ExampleMixin.java | 2 +- src/main/resources/fabric.mod.json | 8 +- 7 files changed, 338 insertions(+), 325 deletions(-) rename src/client/java/{name/modid => mod/theduckman64}/SkinKeybindManagerClient.java (54%) create mode 100644 src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java delete mode 100644 src/client/java/name/modid/SkinKeybindManagerScreen.java rename src/main/java/{name/modid => mod/theduckman64}/SkinKeybindManager.java (96%) rename src/main/java/{name/modid => mod/theduckman64}/mixin/ExampleMixin.java (93%) diff --git a/gradle.properties b/gradle.properties index ef49a93..c109fc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version=0.17.3 loom_version=1.11-SNAPSHOT # Mod Properties -mod_version=1.0.0 +mod_version=1.12.10-fabric maven_group=name.modid archives_base_name=skin-keybind-manager diff --git a/src/client/java/name/modid/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java similarity index 54% rename from src/client/java/name/modid/SkinKeybindManagerClient.java rename to src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index d02534a..d1e7e55 100644 --- a/src/client/java/name/modid/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -1,4 +1,4 @@ -package name.modid; +package mod.theduckman64; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -6,18 +6,20 @@ import com.google.gson.JsonParser; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.fabric.api.client.screen.v1.Screens; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.option.ControlsOptionsScreen; import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.input.KeyInput; import net.minecraft.client.option.KeyBinding; import net.minecraft.client.session.Session; +import net.minecraft.client.toast.SystemToast; import net.minecraft.client.util.InputUtil; import net.minecraft.text.Text; import okhttp3.*; +import org.jetbrains.annotations.NotNull; import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.XZInputStream; import org.tukaani.xz.XZOutputStream; @@ -37,7 +39,7 @@ public class SkinKeybindManagerClient implements ClientModInitializer { {40, 0, 56, 8}, // head overlay 1 {32, 8, 64, 16}, // head overlay 2 {20, 32, 36, 36}, // body overlay 1 - {16, 36, 40, 48}, // body overlay 1 + {16, 36, 40, 48}, // body overlay 2 {44, 32, 52, 36}, // right arm overlay 1 {40, 36, 56, 48}, // right arm overlay 2 {52, 48, 60, 52}, // left arm overlay 1 @@ -45,22 +47,22 @@ public class SkinKeybindManagerClient implements ClientModInitializer { {4, 32, 12, 36}, // right leg overlay 1 {0, 36, 16, 48}, // right leg overlay 2 {4, 48, 12, 52}, // left leg overlay 1 - {0, 52, 16, 54} // left leg overlay 2 + {0, 52, 16, 64} // left leg overlay 2 }; private static final int[][] OVERLAY_RECTS_SLIM = { {40, 0, 56, 8}, // head overlay 1 {32, 8, 64, 16}, // head overlay 2 {20, 32, 36, 36}, // body overlay 1 - {16, 36, 40, 48}, // body overlay 1 - {44, 32, 52, 36}, // right arm overlay 1 - {40, 36, 56, 48}, // right arm overlay 2 - {52, 48, 60, 52}, // left arm overlay 1 - {48, 52, 64, 64}, // left arm overlay 2 + {16, 36, 40, 48}, // body overlay 2 + {44, 32, 50, 36}, // right arm overlay 1 (slim is 3px wide) + {40, 36, 54, 48}, // right arm overlay 2 + {52, 48, 58, 52}, // left arm overlay 1 (slim is 3px wide) + {48, 52, 62, 64}, // left arm overlay 2 {4, 32, 12, 36}, // right leg overlay 1 {0, 36, 16, 48}, // right leg overlay 2 {4, 48, 12, 52}, // left leg overlay 1 - {0, 52, 16, 54} // left leg overlay 2 + {0, 52, 16, 64} // left leg overlay 2 }; private static int[][] getOverlayRects(String variant) { @@ -70,8 +72,7 @@ private static int[][] getOverlayRects(String variant) { @Override public void onInitializeClient() { ClientTickEvents.END_CLIENT_TICK.register(mc -> { - if (ran) return; // run only once - + if (ran) return; session = mc.getSession(); ran = true; }); @@ -82,8 +83,7 @@ public void onInitializeClient() { ButtonWidget myButton = ButtonWidget.builder( Text.literal("Skin Keybind Manager"), - b -> client.setScreen(new SkinKeybindManagerScreen( - screen)) + b -> client.setScreen(new SkinKeybindManagerScreen(screen)) ) .dimensions(5, 5, 150, 20) .build(); @@ -95,16 +95,21 @@ public void onInitializeClient() { public static BufferedImage encodePlayerSkin(List keybindings, BufferedImage skin, String variant) throws Exception { if (skin == null) return null; - // Step 1: Build keybind string + // Step 1: Build keybind string (ID:KEY:TYPE format where TYPE is K or M) StringBuilder sb = new StringBuilder(); sb.append("#SKINKEYBINDS_START\n"); if (keybindings != null && !keybindings.isEmpty()) { for (KeyBinding kb : keybindings) { - String id = kb.getBoundKeyTranslationKey(); - String type = kb.getDefaultKey().getCategory().name(); - int code = kb.getDefaultKey().getCode(); - System.out.println("[SkinKeybindManager] Encoding keybind -> ID: " + id + ", Type: " + type + ", Code: " + code); - sb.append(id).append(":").append(type).append(":").append(code).append(";"); + String id = kb.getId(); + InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); + + // Skip unbound keys (UNKNOWN_KEY has code -1) + if (boundKey == InputUtil.UNKNOWN_KEY || boundKey.getCode() == -1) { + continue; + } + + String translationKey = boundKey.getTranslationKey(); + sb.append(id).append(":").append(translationKey).append(";"); } } sb.append("\n#SKINKEYBINDS_END\n"); @@ -118,127 +123,104 @@ public static BufferedImage encodePlayerSkin(List keybindings, Buffe compressed = baos.toByteArray(); } - // New Step 2.5: Prepare data with a 4-byte length prefix - // The length includes the XZ stream and its footer. + // Step 2.5: Prepare data with a 4-byte length prefix ByteArrayOutputStream finalDataOut = new ByteArrayOutputStream(); - - // Write the length as a 4-byte integer (Big-Endian) finalDataOut.write((compressed.length >> 24) & 0xFF); finalDataOut.write((compressed.length >> 16) & 0xFF); finalDataOut.write((compressed.length >> 8) & 0xFF); finalDataOut.write(compressed.length & 0xFF); - - // Write the actual XZ data finalDataOut.write(compressed); byte[] prefixedCompressed = finalDataOut.toByteArray(); -// Step 3: Write bytes into overlay pixels (use prefixedCompressed) + // Step 3: Write bytes into overlay pixels (ARGB format) int[][] overlayRects = getOverlayRects(variant); int idx = 0; - outer: for (int[] rect : overlayRects) { for (int y = rect[1]; y < rect[3]; y++) { for (int x = rect[0]; x < rect[2]; x++) { - // Use prefixedCompressed here - int r = idx < prefixedCompressed.length ? prefixedCompressed[idx++] & 0xFF : 0; - int g = idx < prefixedCompressed.length ? prefixedCompressed[idx++] & 0xFF : 0; - int b = idx < prefixedCompressed.length ? prefixedCompressed[idx++] & 0xFF : 0; - int a = idx < prefixedCompressed.length ? prefixedCompressed[idx++] & 0xFF : 0; - int rgba = (r << 24) | (g << 16) | (b << 8) | a; - skin.setRGB(x, y, rgba); - if (idx >= prefixedCompressed.length) break outer; + if (idx + 3 < prefixedCompressed.length) { + int a = prefixedCompressed[idx++] & 0xFF; + int r = prefixedCompressed[idx++] & 0xFF; + int g = prefixedCompressed[idx++] & 0xFF; + int b = prefixedCompressed[idx++] & 0xFF; + int argb = (a << 24) | (r << 16) | (g << 8) | b; + skin.setRGB(x, y, argb); + } else { + skin.setRGB(x, y, 0x00000000); + } } } } + return skin; } - - /** * Merge existing skin keybinds with current Fabric keybinds. - * Existing skin keybinds take precedence if present, otherwise use current in-game bindings. + * Current client keybinds take precedence, skin keybinds are used as fallback. */ - public static List getMergedKeybinds(List skinKeybinds) { - Map mergedMap = new LinkedHashMap<>(); - - // Step 1: Start with skin keybinds - if (skinKeybinds != null) { - for (KeyBinding kb : skinKeybinds) { - String keyId = kb.getBoundKeyTranslationKey(); - mergedMap.put(keyId, kb); - System.out.println("[SkinKeybindManager] Skin keybind -> ID: " + keyId + - ", Type: " + kb.getDefaultKey().getTranslationKey() + - ", Code: " + kb.getDefaultKey().getCode()); - } - } - - // Step 2: Merge in current live Fabric keybinds + public static List getMergedKeybinds(Map skinKeybindMap) { MinecraftClient mc = MinecraftClient.getInstance(); + List mergedList = new ArrayList<>(); + for (KeyBinding kb : mc.options.allKeys) { - String keyId = kb.getBoundKeyTranslationKey(); - System.out.println("1" + kb.getDefaultKey()); - System.out.println("2" + kb.getBoundKeyLocalizedText()); - System.out.println("3" + kb.getCategory()); - System.out.println("4" + kb.getId()); - if (!mergedMap.containsKey(keyId)) { - mergedMap.put(keyId, kb); - System.out.println("[SkinKeybindManager] Live keybind -> ID: " + keyId + - ", Type: " + kb.getDefaultKey().getTranslationKey() + - ", Code: " + kb.getDefaultKey().getCode()); + String translationKey = kb.getBoundKeyTranslationKey(); + InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); + + // Always use current client binding if it's set (not UNKNOWN_KEY) + if (boundKey != InputUtil.UNKNOWN_KEY && boundKey.getCode() != -1) { + mergedList.add(kb); + } else if (skinKeybindMap != null && skinKeybindMap.containsKey(translationKey)) { + // Fall back to skin keybind only if client binding is unset + KeyData keyData = skinKeybindMap.get(translationKey); + InputUtil.Key skinKey = InputUtil.fromTranslationKey(keyData.translationKey); + kb.setBoundKey(skinKey); } } - - List mergedList = new ArrayList<>(mergedMap.values()); - System.out.println("[SkinKeybindManager] Total merged keybinds: " + mergedList.size()); return mergedList; } - /** * Decode keybinds from a skin image overlay. - * Returns a list of KeyBinding objects with ID, binding, and category. + * Returns a map of keybind ID -> KeyData (key translation + type) for later application. */ - public static List decodePlayerSkin(BufferedImage skin, String variant) { - List keybindings = new ArrayList<>(); - if (skin == null) return keybindings; + public static Map decodePlayerSkin(BufferedImage skin, String variant) { + Map keybindMap = new HashMap<>(); + if (skin == null) return keybindMap; int[][] overlayRects = getOverlayRects(variant); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - // Step 1: Extract overlay bytes + // Step 1: Extract overlay bytes (ARGB format) for (int[] rect : overlayRects) { for (int y = rect[1]; y < rect[3]; y++) { for (int x = rect[0]; x < rect[2]; x++) { - int rgba = skin.getRGB(x, y); - baos.write((rgba >> 24) & 0xFF); - baos.write((rgba >> 16) & 0xFF); - baos.write((rgba >> 8) & 0xFF); - baos.write(rgba & 0xFF); + int argb = skin.getRGB(x, y); + baos.write((argb >> 24) & 0xFF); // Alpha + baos.write((argb >> 16) & 0xFF); // Red + baos.write((argb >> 8) & 0xFF); // Green + baos.write(argb & 0xFF); // Blue } } } byte[] bytes = baos.toByteArray(); - // Check if there's even space for the 4-byte prefix - if (bytes.length < 4) return keybindings; + if (bytes.length < 4) return keybindMap; // Step 2: Read the 4-byte length prefix - int dataLength = (bytes[0] & 0xFF) << 24 | - (bytes[1] & 0xFF) << 16 | - (bytes[2] & 0xFF) << 8 | + int dataLength = ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); - // Check if the stated length fits within the extracted bytes if (dataLength <= 0 || dataLength > bytes.length - 4) { - System.err.println("[SkinKeybindManager] Invalid or corrupt length prefix."); - return keybindings; + showToast("No prior skin data found"); + return keybindMap; } - // The actual XZ data starts at index 4 and is dataLength long byte[] xzData = Arrays.copyOfRange(bytes, 4, 4 + dataLength); - // Step 3: Decompress XZ safely + // Step 3: Decompress XZ try (XZInputStream xzIn = new XZInputStream(new ByteArrayInputStream(xzData))) { ByteArrayOutputStream decoded = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; @@ -248,7 +230,7 @@ public static List decodePlayerSkin(BufferedImage skin, String varia String result = decoded.toString(); int start = result.indexOf("#SKINKEYBINDS_START"); int end = result.indexOf("#SKINKEYBINDS_END"); - if (start == -1 || end == -1 || start >= end) return keybindings; + if (start == -1 || end == -1 || start >= end) return keybindMap; String data = result.substring(start + "#SKINKEYBINDS_START".length(), end).trim(); if (!data.isEmpty()) { @@ -256,97 +238,101 @@ public static List decodePlayerSkin(BufferedImage skin, String varia for (String entry : entries) { if (entry.isEmpty()) continue; String[] parts = entry.split(":"); - if (parts.length >= 3) { + if (parts.length == 2) { String id = parts[0]; - InputUtil.Type type = InputUtil.Type.valueOf(parts[1]); - int code = Integer.parseInt(parts[2]); - InputUtil.Key boundKey = type.createFromCode(code); - keybindings.add(new KeyBinding(id, type, code, KeyBinding.Category.MISC)); + String translationKey = parts[1]; + keybindMap.put(id, new KeyData(translationKey, id)); } } } - } catch (Exception e) { - System.out.println("[SkinKeybindManager] No keybinds found or decompression failed: " + e); - // Return empty list if nothing encoded yet - } + } catch (Exception ignored) {} + return keybindMap; + } + + /** + * Simple data class to hold key translation and type + */ + public static class KeyData { + public final String translationKey; + public final String id; - return keybindings; + public KeyData(String keyTranslationKey, String id) { + this.translationKey = keyTranslationKey; + this.id = id; + } } + public static void showToast(String message) { + MinecraftClient.getInstance().getToastManager().add( + SystemToast.create( + MinecraftClient.getInstance(), + SystemToast.Type.NARRATOR_TOGGLE, + Text.literal("Skin Keybind Manager"), + Text.literal(message) + ) + ); + } - public static int applyKeybinds(List fromSkinBindings) { - if (fromSkinBindings == null) fromSkinBindings = List.of(); + /** + * Apply keybinds from a decoded map (translationKey -> KeyData). + * Matches keybinds by translation key with existing client keybinds. + * Always overwrites existing bindings with the skin's bindings. + */ + public static int applyKeybinds(Map skinKeybindMap) { + if (skinKeybindMap == null || skinKeybindMap.isEmpty()) return 0; MinecraftClient mc = MinecraftClient.getInstance(); KeyBinding[] allKeys = mc.options.allKeys; int applied = 0; for (KeyBinding kb : allKeys) { - // Find matching key in fromSkinBindings - KeyBinding match = fromSkinBindings.stream() - .filter(f -> f.getBoundKeyTranslationKey().equals(kb.getBoundKeyTranslationKey())) - .findFirst() - .orElse(null); - - if (match != null) { - kb.setBoundKey(match.getDefaultKey()); + String id = kb.getId(); + + if (skinKeybindMap.containsKey(id)) { + KeyData keyData = skinKeybindMap.get(id); + + // Parse the key from its translation key + InputUtil.Key newKey = InputUtil.fromTranslationKey(keyData.translationKey); + + kb.setBoundKey(newKey); applied++; - } else { - // Unbind the key - kb.setBoundKey(InputUtil.UNKNOWN_KEY); } } KeyBinding.updateKeysByCode(); - System.out.println("[SkinKeybindManager] Applied " + applied + " keybinds. Others unbound."); + showToast("[SkinKeybindManager] Applied " + applied + " keybinds from skin."); return applied; } - - - - - public static void saveSkinToDisk(BufferedImage skin) throws IOException { + public static @NotNull File saveSkinToDisk(BufferedImage skin) throws IOException { File out = new File(FabricLoader.getInstance().getGameDir() + "/" + "skin_encoded.png"); ImageIO.write(skin, "PNG", out); - System.out.println("[SkinKeybindManager] Encoded skin saved: " + out.getAbsolutePath()); + return out; } - public static BufferedImage loadSkinFromDisk() { try { - // Build the file path inside the game directory File skinFile = new File(FabricLoader.getInstance().getGameDir() + "/" + "skin_encoded.png"); - if (!skinFile.exists()) { - System.err.println("[SkinKeybindManager] Skin file not found: " + skinFile.getAbsolutePath()); + showToast("No skin on disk"); return null; } - - // Read PNG into BufferedImage - BufferedImage skin = ImageIO.read(skinFile); - System.out.println("[SkinKeybindManager] Loaded skin from: " + skinFile.getAbsolutePath()); - return skin; - - } catch (IOException e) { - System.err.println("[SkinKeybindManager] Failed to load skin: " + e.getMessage()); + return ImageIO.read(skinFile); + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); return null; } } public static BufferedImage downloadSkin(String username) throws Exception { - - // 1. Fetch UUID from Mojang API - String uuid = getProfileUUID(username); // your existing method + String uuid = getProfileUUID(username); if (uuid == null) { - System.err.println("[SkinKeybindManager] Could not fetch UUID for username: " + username); - uuid = "fcab5be823974c298ff904911c72e294"; - //return null; + showToast("Error: UUID not found"); } - // 2. Fetch session server profile to get skin URL - String accessToken = session.getAccessToken(); // your session object - String response = getSessionServerProfile(uuid, accessToken); // your existing method + String accessToken = session.getAccessToken(); + String response = getSessionServerProfile(uuid, accessToken); JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); JsonArray properties = profileJson.getAsJsonArray("properties"); @@ -358,68 +344,51 @@ public static BufferedImage downloadSkin(String username) throws Exception { break; } } + if (base64Value == null) { - System.err.println("[SkinKeybindManager] No 'textures' property found for user: " + username); return null; } - // 3. Decode base64 JSON to get skin URL String decodedJson = new String(java.util.Base64.getDecoder().decode(base64Value)); JsonObject texturesJson = JsonParser.parseString(decodedJson).getAsJsonObject().getAsJsonObject("textures"); if (!texturesJson.has("SKIN")) return null; String skinUrl = texturesJson.getAsJsonObject("SKIN").get("url").getAsString(); - // 4. Download skin image Request request = new Request.Builder().url(skinUrl).build(); try (Response resp = client.newCall(request).execute()) { if (!resp.isSuccessful()) { - System.err.println("[SkinKeybindManager] Failed to download skin: " + resp.code()); + showToast("Failed to download skin: " + resp.code()); return null; } - byte[] imageBytes = resp.body().bytes(); - return ImageIO.read(new ByteArrayInputStream(imageBytes)); + byte[] imageBytes; + assert resp.body() != null; + imageBytes = resp.body().bytes(); + return ImageIO.read(new ByteArrayInputStream(imageBytes)); } } public static String getVariant(BufferedImage skin) { if (skin == null) return "classic"; - - // Coordinates for the left arm overlay pixel in 64x64 skin int x = 54; int y = 20; - - // Make sure the skin is at least 64x64 if (skin.getWidth() < 64 || skin.getHeight() < 64) return "classic"; - int rgba = skin.getRGB(x, y); int alpha = (rgba >> 24) & 0xFF; - return (alpha != 0) ? "slim" : "classic"; } - /** - * Uploads a skin file to Mojang using the current player's session. - * - * @param skinFile PNG file with your overlay - * @param slim true for slim (Alex) model, false for classic - * @return true if upload succeeded - */ - public boolean uploadSkin(File skinFile, boolean slim) throws IOException { + public static boolean uploadSkin(File skinFile, String slim) throws IOException { MinecraftClient mc = MinecraftClient.getInstance(); Session session = mc.getSession(); - String username = session.getUsername(); String accessToken = session.getAccessToken(); - String uuid = getProfileUUID(username); - String url = "https://api.minecraft.net/session/minecraft/profile/" + uuid + "/skin"; + String url = "https://api.minecraftservices.com/minecraft/profile/skins"; MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); bodyBuilder.addFormDataPart("file", skinFile.getName(), RequestBody.create(skinFile, MediaType.parse("image/png"))); - if (slim) { - bodyBuilder.addFormDataPart("model", "slim"); - } + bodyBuilder.addFormDataPart("variant", slim); Request request = new Request.Builder() .url(url) @@ -429,7 +398,7 @@ public boolean uploadSkin(File skinFile, boolean slim) throws IOException { try (Response resp = client.newCall(request).execute()) { if (!resp.isSuccessful()) { - System.err.println("[SkinKeybindManager] Failed to upload skin: " + resp.code() + " " + resp.message()); + showToast("Failed to upload skin: " + resp.code() + " " + resp.message()); return false; } return true; @@ -438,15 +407,12 @@ public boolean uploadSkin(File skinFile, boolean slim) throws IOException { private static String getProfileUUID(String username) throws IOException { String url = "https://api.mojang.com/users/profiles/minecraft/" + username; - System.out.println("URL: " + url); Request request = new Request.Builder().url(url).build(); try (Response resp = client.newCall(request).execute()) { if (!resp.isSuccessful()) return null; - String body = resp.body().string(); - System.out.println("BODY: " + body); + assert resp.body() != null; + String body = resp.body().string(); JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - System.out.println("JSON: " + json); - System.out.println("ID: " + json.get("id").getAsString()); return json.get("id").getAsString(); } } @@ -460,7 +426,8 @@ private static String getSessionServerProfile(String uuid, String accessToken) t try (Response resp = client.newCall(request).execute()) { if (!resp.isSuccessful()) throw new IOException("[SkinKeybindManager] Session server HTTP error: " + resp); - return resp.body().string(); + assert resp.body() != null; + return resp.body().string(); } } -} +} \ No newline at end of file diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java new file mode 100644 index 0000000..d383742 --- /dev/null +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java @@ -0,0 +1,190 @@ +package mod.theduckman64; + +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.text.Text; +import net.minecraft.client.toast.SystemToast; + +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Map; + +public class SkinKeybindManagerScreen extends Screen { + + private final Screen parent; + private static long lastUploadTime = 0; + private static final long UPLOAD_COOLDOWN_MS = 10000; // 10 seconds + + public SkinKeybindManagerScreen(Screen parent) { + super(Text.of("Skin Keybind Manager")); + this.parent = parent; + } + + @Override + protected void init() { + // Back button + ButtonWidget backButton = ButtonWidget.builder( + Text.of("Back"), + b -> client.setScreen(parent)) + .dimensions(5, 5, 150, 20) + .build(); + Screens.getButtons(this).add(backButton); + + ButtonWidget uploadButton = ButtonWidget.builder( + Text.of("Upload keybinds to skin"), + b -> { + try { + long currentTime = System.currentTimeMillis(); + long timeSinceLastUpload = currentTime - lastUploadTime; + + if (timeSinceLastUpload < UPLOAD_COOLDOWN_MS) { + long remainingMs = UPLOAD_COOLDOWN_MS - timeSinceLastUpload; + long remainingSeconds = (remainingMs + 999) / 1000; // Round up + showToast("Please wait " + remainingSeconds + " seconds (API Limit reached)"); + return; + } + // Download current skin + BufferedImage skin = SkinKeybindManagerClient.downloadSkin( + SkinKeybindManagerClient.session.getUsername() + ); + String variant = SkinKeybindManagerClient.getVariant(skin); + + // Decode existing skin keybinds + Map skinKeybinds = + SkinKeybindManagerClient.decodePlayerSkin(skin, variant); + + // Merge with current client bindings (client takes priority) + List mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + + // Encode merged bindings into skin + BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( + mergedBindings, + skin, + variant); + + boolean success = SkinKeybindManagerClient.uploadSkin( + SkinKeybindManagerClient.saveSkinToDisk(encodedSkin), + variant); + + if (success) { + showToast("Successfully uploaded keybinds to skin"); + } else { + showToast("Failed to upload skin"); + } + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); + } + }) + .dimensions(5, 30, 150, 20) + .build(); + Screens.getButtons(this).add(uploadButton); + + ButtonWidget downloadButton = ButtonWidget.builder( + Text.of("Download keybinds from skin"), + b -> { + try { + BufferedImage skin = SkinKeybindManagerClient.downloadSkin( + SkinKeybindManagerClient.session.getUsername() + ); + + Map keybinds = + SkinKeybindManagerClient.decodePlayerSkin( + skin, + SkinKeybindManagerClient.getVariant(skin) + ); + + int applied = SkinKeybindManagerClient.applyKeybinds(keybinds); + + showToast("Applied " + applied + " keybinds from skin."); + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); + } + }) + .dimensions(160, 30, 150, 20) + .build(); + Screens.getButtons(this).add(downloadButton); + + + // Save PNG to disk (encode online skin with merged keybinds and save) + ButtonWidget toDiskButton = ButtonWidget.builder( + Text.of("Save PNG to disk"), + b -> { + try { + BufferedImage skin = SkinKeybindManagerClient.downloadSkin( + SkinKeybindManagerClient.session.getUsername() + ); + String variant = SkinKeybindManagerClient.getVariant(skin); + + // Decode skin keybinds and merge with client + Map skinKeybinds = + SkinKeybindManagerClient.decodePlayerSkin(skin, variant); + List mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + + // Encode and save + BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( + mergedBindings, + skin, + variant + ); + SkinKeybindManagerClient.saveSkinToDisk(encodedSkin); + showToast("Saved skin_encoded.png to disk."); + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); + } + }) + .dimensions(5, 55, 150, 20) + .build(); + Screens.getButtons(this).add(toDiskButton); + + // Load keybinds from disk + ButtonWidget fromDiskButton = ButtonWidget.builder( + Text.of("Load keybinds from disk"), + b -> { + try { + BufferedImage skin = SkinKeybindManagerClient.loadSkinFromDisk(); + if (skin == null) { + System.err.println("[SkinKeybindManagerScreen] No skin file found on disk."); + return; + } + + Map decoded = + SkinKeybindManagerClient.decodePlayerSkin( + skin, + SkinKeybindManagerClient.getVariant(skin) + ); + int applied = SkinKeybindManagerClient.applyKeybinds(decoded); + showToast("[SkinKeybindManagerScreen] Applied " + applied + " keybinds from disk skin."); + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); + } + }) + .dimensions(160, 55, 150, 20) + .build(); + Screens.getButtons(this).add(fromDiskButton); + + } + + public void showToast(String message) { + MinecraftClient.getInstance().getToastManager().add( + SystemToast.create( + MinecraftClient.getInstance(), + SystemToast.Type.NARRATOR_TOGGLE, + Text.literal("Skin Keybind Manager"), + Text.literal(message) + ) + ); + } + + + @Override + public void close() { + client.setScreen(parent); + } +} \ No newline at end of file diff --git a/src/client/java/name/modid/SkinKeybindManagerScreen.java b/src/client/java/name/modid/SkinKeybindManagerScreen.java deleted file mode 100644 index e116eb5..0000000 --- a/src/client/java/name/modid/SkinKeybindManagerScreen.java +++ /dev/null @@ -1,144 +0,0 @@ -package name.modid; - -import net.fabricmc.fabric.api.client.screen.v1.Screens; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.option.ControlsOptionsScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.option.KeyBinding; -import net.minecraft.text.Text; - -import java.awt.image.BufferedImage; -import java.util.List; - -public class SkinKeybindManagerScreen extends Screen { - - private final Screen parent; - - public SkinKeybindManagerScreen(Screen parent) { - super(Text.of("Skin Keybind Manager")); - this.parent = parent; - } - - @Override - protected void init() { - // Back button - ButtonWidget backButton = ButtonWidget.builder( - Text.of("Back"), - b -> client.setScreen(parent)) - .dimensions(5, 5, 150, 20) - .build(); - Screens.getButtons(this).add(backButton); - - // Upload keybinds to skin (download live) - ButtonWidget encodeButton = ButtonWidget.builder( - Text.of("Upload keybinds to skin"), - b -> { - try { - BufferedImage skin = SkinKeybindManagerClient.downloadSkin( - SkinKeybindManagerClient.session.getUsername() - ); - String variant = SkinKeybindManagerClient.getVariant(skin); - - List bindings = SkinKeybindManagerClient.decodePlayerSkin( - skin, - variant - ); - - BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( - bindings, - skin, - variant); - SkinKeybindManagerClient.saveSkinToDisk(encodedSkin); - - System.out.println("[SkinKeybindManagerScreen] Skin encoded and saved successfully."); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .dimensions(5, 30, 150, 20) - .build(); - Screens.getButtons(this).add(encodeButton); - - // Download keybinds from skin (download live) - ButtonWidget decodeButton = ButtonWidget.builder( - Text.of("Download keybinds from skin"), - b -> { - try { - BufferedImage skin = SkinKeybindManagerClient.downloadSkin( - SkinKeybindManagerClient.session.getUsername() - ); - - List bindings = SkinKeybindManagerClient.decodePlayerSkin( - skin, - SkinKeybindManagerClient.getVariant(skin) - ); - - SkinKeybindManagerClient.applyKeybinds(bindings); - - System.out.println("[SkinKeybindManagerScreen] Keybinds decoded and applied."); - } catch (Exception e) { - throw new RuntimeException(e); - } - }) - .dimensions(160, 30, 150, 20) - .build(); - Screens.getButtons(this).add(decodeButton); - - - // Save PNG to disk (encode online skin and save) - ButtonWidget toDiskButton = ButtonWidget.builder( - Text.of("Save PNG to disk"), - b -> { - try { - BufferedImage skin = SkinKeybindManagerClient.downloadSkin( - SkinKeybindManagerClient.session.getUsername() - ); - List currentBindings = SkinKeybindManagerClient.getMergedKeybinds( - SkinKeybindManagerClient.decodePlayerSkin( - skin, - SkinKeybindManagerClient.getVariant(skin) - ) - ); - BufferedImage encoded = SkinKeybindManagerClient.encodePlayerSkin( - currentBindings, - skin, - SkinKeybindManagerClient.getVariant(skin) - ); - SkinKeybindManagerClient.saveSkinToDisk(encoded); - System.out.println("[SkinKeybindManagerScreen] Skin with keybinds saved to disk."); - } catch (Exception e) { - e.printStackTrace(); - } - }) - .dimensions(320, 30, 150, 20) - .build(); - Screens.getButtons(this).add(toDiskButton); - - // Load keybinds from disk - ButtonWidget fromDiskButton = ButtonWidget.builder( - Text.of("Load keybinds from disk"), - b -> { - try { - BufferedImage skin = SkinKeybindManagerClient.loadSkinFromDisk(); - if (skin == null) return; - List decoded = SkinKeybindManagerClient.decodePlayerSkin( - skin, - SkinKeybindManagerClient.getVariant(skin) - ); - int applied = SkinKeybindManagerClient.applyKeybinds(decoded); - System.out.println("[SkinKeybindManagerScreen] Applied " + applied + " keybinds from disk skin."); - } catch (Exception e) { - e.printStackTrace(); - } - }) - .dimensions(5, 60, 150, 20) - .build(); - Screens.getButtons(this).add(fromDiskButton); - } - - @Override - public void close() { - client.setScreen(parent); - } -} diff --git a/src/main/java/name/modid/SkinKeybindManager.java b/src/main/java/mod/theduckman64/SkinKeybindManager.java similarity index 96% rename from src/main/java/name/modid/SkinKeybindManager.java rename to src/main/java/mod/theduckman64/SkinKeybindManager.java index a6c6e76..ced09a7 100644 --- a/src/main/java/name/modid/SkinKeybindManager.java +++ b/src/main/java/mod/theduckman64/SkinKeybindManager.java @@ -1,4 +1,4 @@ -package name.modid; +package mod.theduckman64; import net.fabricmc.api.ModInitializer; diff --git a/src/main/java/name/modid/mixin/ExampleMixin.java b/src/main/java/mod/theduckman64/mixin/ExampleMixin.java similarity index 93% rename from src/main/java/name/modid/mixin/ExampleMixin.java rename to src/main/java/mod/theduckman64/mixin/ExampleMixin.java index bcb3971..f6e0fc5 100644 --- a/src/main/java/name/modid/mixin/ExampleMixin.java +++ b/src/main/java/mod/theduckman64/mixin/ExampleMixin.java @@ -1,4 +1,4 @@ -package name.modid.mixin; +package mod.theduckman64.mixin; import net.minecraft.server.MinecraftServer; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index bc5e7e8..f204531 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,22 +3,22 @@ "id": "skin-keybind-manager", "version": "${version}", "name": "Skin Keybind Manager", - "description": "This is an example description! Tell everyone what your mod is about!", + "description": "Stores keybinds in the skin overlay", "authors": [ "TheDuckMan64" ], "contact": { - "sources": "https://github.com/FabricMC/fabric-example-mod" + "sources": "https://github.com/TheDuckMan64/SkinKeybindManager" }, "license": "CC0-1.0", "icon": "assets/skin-keybind-manager/icon.png", "environment": "*", "entrypoints": { "main": [ - "name.modid.SkinKeybindManager" + "mod.theduckman64.SkinKeybindManager" ], "client": [ - "name.modid.SkinKeybindManagerClient" + "mod.theduckman64.SkinKeybindManagerClient" ] }, "mixins": [ From 81dbaf147209eedecba92d82333fd54994b35dc4 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 6 Nov 2025 14:19:42 +1100 Subject: [PATCH 02/10] 1.21.10-fabric --- gradle.properties | 4 +- .../SkinKeybindManagerClient.java | 106 ++++++++++++------ .../SkinKeybindManagerScreen.java | 16 ++- 3 files changed, 87 insertions(+), 39 deletions(-) diff --git a/gradle.properties b/gradle.properties index c109fc3..22a9f7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,8 +13,8 @@ loader_version=0.17.3 loom_version=1.11-SNAPSHOT # Mod Properties -mod_version=1.12.10-fabric -maven_group=name.modid +mod_version=1.12.10 +maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager # Dependencies diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index d1e7e55..0a2c407 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -36,33 +36,45 @@ public class SkinKeybindManagerClient implements ClientModInitializer { public static Session session; private static final int[][] OVERLAY_RECTS_CLASSIC = { - {40, 0, 56, 8}, // head overlay 1 - {32, 8, 64, 16}, // head overlay 2 - {20, 32, 36, 36}, // body overlay 1 - {16, 36, 40, 48}, // body overlay 2 - {44, 32, 52, 36}, // right arm overlay 1 - {40, 36, 56, 48}, // right arm overlay 2 - {52, 48, 60, 52}, // left arm overlay 1 - {48, 52, 64, 64}, // left arm overlay 2 - {4, 32, 12, 36}, // right leg overlay 1 - {0, 36, 16, 48}, // right leg overlay 2 - {4, 48, 12, 52}, // left leg overlay 1 - {0, 52, 16, 64} // left leg overlay 2 + {0, 0, 8, 8}, + {24, 0, 40, 8}, + {56, 0, 64, 8}, + {0, 16, 4, 20}, + {12, 16, 20, 20}, + {36, 16, 44, 20}, + {52, 16, 56, 20}, + {0, 32, 4, 36}, + {12, 32, 20, 36}, + {36, 32, 44, 36}, + {52, 32, 56, 36}, + {0, 48, 4, 52}, + {12, 48, 20, 52}, + {28, 48, 36, 52}, + {44, 48, 52, 52}, + {60, 48, 64, 52}, + {56, 16, 64, 48} }; private static final int[][] OVERLAY_RECTS_SLIM = { - {40, 0, 56, 8}, // head overlay 1 - {32, 8, 64, 16}, // head overlay 2 - {20, 32, 36, 36}, // body overlay 1 - {16, 36, 40, 48}, // body overlay 2 - {44, 32, 50, 36}, // right arm overlay 1 (slim is 3px wide) - {40, 36, 54, 48}, // right arm overlay 2 - {52, 48, 58, 52}, // left arm overlay 1 (slim is 3px wide) - {48, 52, 62, 64}, // left arm overlay 2 - {4, 32, 12, 36}, // right leg overlay 1 - {0, 36, 16, 48}, // right leg overlay 2 - {4, 48, 12, 52}, // left leg overlay 1 - {0, 52, 16, 64} // left leg overlay 2 + {0, 0, 8, 8}, + {24, 0, 40, 8}, + {56, 0, 64, 8}, + {0, 16, 4, 20}, + {12, 16, 20, 20}, + {36, 16, 44, 20}, + {50, 16, 54, 20}, + {0, 32, 4, 36}, + {12, 32, 20, 36}, + {36, 32, 44, 36}, + {50, 32, 54, 36}, + {0, 48, 4, 52}, + {12, 48, 20, 52}, + {28, 48, 36, 52}, + {42, 48, 52, 52}, + {46, 52, 48, 64}, + {58, 48, 64, 52}, + {54, 16, 64, 48}, + {62, 52, 64, 64} }; private static int[][] getOverlayRects(String variant) { @@ -79,7 +91,6 @@ public void onInitializeClient() { ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { if (!(screen instanceof ControlsOptionsScreen)) return; - System.out.println("ControlOptionsScreen"); ButtonWidget myButton = ButtonWidget.builder( Text.literal("Skin Keybind Manager"), @@ -368,14 +379,43 @@ public static BufferedImage downloadSkin(String username) throws Exception { } } - public static String getVariant(BufferedImage skin) { - if (skin == null) return "classic"; - int x = 54; - int y = 20; - if (skin.getWidth() < 64 || skin.getHeight() < 64) return "classic"; - int rgba = skin.getRGB(x, y); - int alpha = (rgba >> 24) & 0xFF; - return (alpha != 0) ? "slim" : "classic"; + public static String getVariant(String username) throws IOException { + String uuid = getProfileUUID(username); + String accessToken = session.getAccessToken(); + + // Get the profile JSON from Mojang session server + String response = getSessionServerProfile(uuid, accessToken); + JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); + JsonArray properties = profileJson.getAsJsonArray("properties"); + + String base64Value = null; + for (JsonElement e : properties) { + JsonObject prop = e.getAsJsonObject(); + if ("textures".equals(prop.get("name").getAsString())) { + base64Value = prop.get("value").getAsString(); + break; + } + } + + if (base64Value == null) { + System.err.println("[SkinFetcher] No 'textures' property found for user: " + username); + return "classic"; // default fallback + } + + // Decode the Base64 value + String decodedJson = new String(Base64.getDecoder().decode(base64Value)); + JsonObject texturesJson = JsonParser.parseString(decodedJson) + .getAsJsonObject() + .getAsJsonObject("textures"); + + if (!texturesJson.has("SKIN")) return "classic"; + + JsonObject skinObject = texturesJson.getAsJsonObject("SKIN"); + + // Return the model variant if present, otherwise "classic" + return skinObject.has("metadata") && skinObject.getAsJsonObject("metadata").has("model") + ? skinObject.getAsJsonObject("metadata").get("model").getAsString() + : "classic"; } public static boolean uploadSkin(File skinFile, String slim) throws IOException { diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java index d383742..af0adfe 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java @@ -50,7 +50,9 @@ protected void init() { BufferedImage skin = SkinKeybindManagerClient.downloadSkin( SkinKeybindManagerClient.session.getUsername() ); - String variant = SkinKeybindManagerClient.getVariant(skin); + String variant = SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ); // Decode existing skin keybinds Map skinKeybinds = @@ -94,7 +96,9 @@ protected void init() { Map keybinds = SkinKeybindManagerClient.decodePlayerSkin( skin, - SkinKeybindManagerClient.getVariant(skin) + SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ) ); int applied = SkinKeybindManagerClient.applyKeybinds(keybinds); @@ -118,7 +122,9 @@ protected void init() { BufferedImage skin = SkinKeybindManagerClient.downloadSkin( SkinKeybindManagerClient.session.getUsername() ); - String variant = SkinKeybindManagerClient.getVariant(skin); + String variant = SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ); // Decode skin keybinds and merge with client Map skinKeybinds = @@ -156,7 +162,9 @@ protected void init() { Map decoded = SkinKeybindManagerClient.decodePlayerSkin( skin, - SkinKeybindManagerClient.getVariant(skin) + SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ) ); int applied = SkinKeybindManagerClient.applyKeybinds(decoded); showToast("[SkinKeybindManagerScreen] Applied " + applied + " keybinds from disk skin."); From 63d8a9e4594b11ee5ccb9c152973269f5ed799ec Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 6 Nov 2025 14:35:04 +1100 Subject: [PATCH 03/10] 1.21.x-fabric --- build.gradle | 3 +++ gradle.properties | 2 +- .../java/mod/theduckman64/SkinKeybindManagerClient.java | 1 - src/main/resources/fabric.mod.json | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 62d5430..c0c3b52 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,10 @@ dependencies { include 'com.squareup.okio:okio:3.3.0' include 'com.squareup.okhttp3:okhttp:4.12.0' include 'com.google.code.gson:gson:2.10.1' + include 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' implementation 'org.tukaani:xz:1.10' + implementation 'com.squareup.okio:okio:3.3.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.10.1' // To change the versions see the gradle.properties file diff --git a/gradle.properties b/gradle.properties index 22a9f7e..d4b8970 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version=0.17.3 loom_version=1.11-SNAPSHOT # Mod Properties -mod_version=1.12.10 +mod_version=1.21.10 maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index 0a2c407..204f8a8 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -311,7 +311,6 @@ public static int applyKeybinds(Map skinKeybindMap) { } KeyBinding.updateKeysByCode(); - showToast("[SkinKeybindManager] Applied " + applied + " keybinds from skin."); return applied; } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f204531..bc2ede3 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -24,8 +24,8 @@ "mixins": [ ], "depends": { - "fabricloader": ">=0.17.3", - "minecraft": "~1.21.10", + "fabricloader": ">=0.15", + "minecraft": "1.21.*", "java": ">=21", "fabric-api": "*" }, From 387156b214e9cfed7e1f0dfb0697b652f14ebf3d Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 6 Nov 2025 14:41:44 +1100 Subject: [PATCH 04/10] Update getMergedKeybinds() --- src/client/java/mod/theduckman64/SkinKeybindManagerClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index 204f8a8..5958d75 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -186,6 +186,7 @@ public static List getMergedKeybinds(Map skinKeybin KeyData keyData = skinKeybindMap.get(translationKey); InputUtil.Key skinKey = InputUtil.fromTranslationKey(keyData.translationKey); kb.setBoundKey(skinKey); + mergedList.add(kb); } } return mergedList; @@ -469,4 +470,4 @@ private static String getSessionServerProfile(String uuid, String accessToken) t return resp.body().string(); } } -} \ No newline at end of file +} From b0ba81fa842651f0277d326ad86355b5aefd03f1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Thu, 6 Nov 2025 20:49:33 +1100 Subject: [PATCH 05/10] SKM 1.21.x --- build.gradle | 2 +- gradle.properties | 2 +- src/main/resources/fabric.mod.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c0c3b52..2a25972 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ dependencies { include 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' implementation 'org.tukaani:xz:1.10' - implementation 'com.squareup.okio:okio:3.3.0' + implementation 'com.squareup.okio:okio:3.4.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.10.1' // To change the versions see the gradle.properties file diff --git a/gradle.properties b/gradle.properties index d4b8970..1cd47f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version=0.17.3 loom_version=1.11-SNAPSHOT # Mod Properties -mod_version=1.21.10 +mod_version=1.21 maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index bc2ede3..ffcf0e4 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -8,7 +8,7 @@ "TheDuckMan64" ], "contact": { - "sources": "https://github.com/TheDuckMan64/SkinKeybindManager" + "sources": "https://github.com/TheDuckMan64/Skin-Keybind-Manager" }, "license": "CC0-1.0", "icon": "assets/skin-keybind-manager/icon.png", From 06ec088db85ba8ca7153bb10936502fb0cfe457b Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 7 Nov 2025 13:49:15 +1100 Subject: [PATCH 06/10] 1.21.9-1.21.10 --- gradle.properties | 14 +++++++------- .../mod/theduckman64/SkinKeybindManagerClient.java | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1cd47f6..c9aeccb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,15 +7,15 @@ org.gradle.configuration-cache=false # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.10 -yarn_mappings=1.21.10+build.2 +minecraft_version=1.21.9 +yarn_mappings=1.21.9+build.1 loader_version=0.17.3 -loom_version=1.11-SNAPSHOT +loom_version=1.12-SNAPSHOT + +# Fabric API +fabric_version=0.134.0+1.21.9 # Mod Properties -mod_version=1.21 +mod_version=1.21.9 maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager - -# Dependencies -fabric_version=0.136.0+1.21.10 diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index 5958d75..35ae869 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -112,6 +112,7 @@ public static BufferedImage encodePlayerSkin(List keybindings, Buffe if (keybindings != null && !keybindings.isEmpty()) { for (KeyBinding kb : keybindings) { String id = kb.getId(); + //String id = kb.getId(); InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); // Skip unbound keys (UNKNOWN_KEY has code -1) From 0e6d1e53b8ffa17048b116ee00a91e7e499849d1 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 7 Nov 2025 14:09:30 +1100 Subject: [PATCH 07/10] 1.21.x --- gradle.properties | 9 +++++---- .../java/mod/theduckman64/SkinKeybindManagerClient.java | 5 +++-- .../java/mod/theduckman64/SkinKeybindManagerScreen.java | 1 + 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index c9aeccb..e902db6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,15 +7,16 @@ org.gradle.configuration-cache=false # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.9 -yarn_mappings=1.21.9+build.1 + +minecraft_version=1.21 +yarn_mappings=1.21+build.9 loader_version=0.17.3 loom_version=1.12-SNAPSHOT # Fabric API -fabric_version=0.134.0+1.21.9 +fabric_version=0.102.0+1.21 # Mod Properties -mod_version=1.21.9 +mod_version=1.21.0 maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index 35ae869..402dc59 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -111,7 +111,7 @@ public static BufferedImage encodePlayerSkin(List keybindings, Buffe sb.append("#SKINKEYBINDS_START\n"); if (keybindings != null && !keybindings.isEmpty()) { for (KeyBinding kb : keybindings) { - String id = kb.getId(); + String id = kb.getTranslationKey(); //String id = kb.getId(); InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); @@ -299,9 +299,10 @@ public static int applyKeybinds(Map skinKeybindMap) { int applied = 0; for (KeyBinding kb : allKeys) { - String id = kb.getId(); + String id = kb.getTranslationKey(); if (skinKeybindMap.containsKey(id)) { + System.out.println("ID Found: " + id); KeyData keyData = skinKeybindMap.get(id); // Parse the key from its translation key diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java index af0adfe..2935d0d 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java @@ -73,6 +73,7 @@ protected void init() { if (success) { showToast("Successfully uploaded keybinds to skin"); + lastUploadTime = System.currentTimeMillis(); } else { showToast("Failed to upload skin"); } From c057cb8bbe07fd14399933ad57f9e98edeb27dc5 Mon Sep 17 00:00:00 2001 From: Hamish Date: Fri, 7 Nov 2025 14:18:47 +1100 Subject: [PATCH 08/10] 1.21.x --- gradle.properties | 2 +- .../SkinKeybindManagerClient.java | 61 ++++++++++--------- .../SkinKeybindManagerScreen.java | 4 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/gradle.properties b/gradle.properties index e902db6..de2dece 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,6 @@ loom_version=1.12-SNAPSHOT fabric_version=0.102.0+1.21 # Mod Properties -mod_version=1.21.0 +mod_version=1.21 maven_group=mod.theduckman64 archives_base_name=skin-keybind-manager diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java index 402dc59..39d395e 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java @@ -103,25 +103,17 @@ public void onInitializeClient() { }); } - public static BufferedImage encodePlayerSkin(List keybindings, BufferedImage skin, String variant) throws Exception { + public static BufferedImage encodePlayerSkin(Map keybindMap, BufferedImage skin, String variant) throws Exception { if (skin == null) return null; // Step 1: Build keybind string (ID:KEY:TYPE format where TYPE is K or M) StringBuilder sb = new StringBuilder(); sb.append("#SKINKEYBINDS_START\n"); - if (keybindings != null && !keybindings.isEmpty()) { - for (KeyBinding kb : keybindings) { - String id = kb.getTranslationKey(); - //String id = kb.getId(); - InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); - - // Skip unbound keys (UNKNOWN_KEY has code -1) - if (boundKey == InputUtil.UNKNOWN_KEY || boundKey.getCode() == -1) { - continue; - } - - String translationKey = boundKey.getTranslationKey(); - sb.append(id).append(":").append(translationKey).append(";"); + if (keybindMap != null && !keybindMap.isEmpty()) { + for (Map.Entry entry : keybindMap.entrySet()) { + String id = entry.getKey(); + KeyData keyData = entry.getValue(); + sb.append(id).append(":").append(keyData.translationKey).append(";"); } } sb.append("\n#SKINKEYBINDS_END\n"); @@ -169,28 +161,42 @@ public static BufferedImage encodePlayerSkin(List keybindings, Buffe /** * Merge existing skin keybinds with current Fabric keybinds. - * Current client keybinds take precedence, skin keybinds are used as fallback. + * Returns a map of keybind ID -> KeyData with the following logic: + * 1) If the key exists (is bound) in the client, add it to the output + * 2) If the key exists but is unbound in the client, remove it from skinKeybindMap + * 3) Add all leftover items in skinKeybindMap to the output */ - public static List getMergedKeybinds(Map skinKeybindMap) { + public static Map getMergedKeybinds(Map skinKeybindMap) { MinecraftClient mc = MinecraftClient.getInstance(); - List mergedList = new ArrayList<>(); + Map mergedMap = new HashMap<>(); + + // Create a copy of skinKeybindMap to track leftovers + Map remainingSkinKeybinds = skinKeybindMap != null + ? new HashMap<>(skinKeybindMap) + : new HashMap<>(); for (KeyBinding kb : mc.options.allKeys) { - String translationKey = kb.getBoundKeyTranslationKey(); + String id = kb.getTranslationKey(); InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); - // Always use current client binding if it's set (not UNKNOWN_KEY) + // Check if key is bound (not UNKNOWN_KEY and code != -1) if (boundKey != InputUtil.UNKNOWN_KEY && boundKey.getCode() != -1) { - mergedList.add(kb); - } else if (skinKeybindMap != null && skinKeybindMap.containsKey(translationKey)) { - // Fall back to skin keybind only if client binding is unset - KeyData keyData = skinKeybindMap.get(translationKey); - InputUtil.Key skinKey = InputUtil.fromTranslationKey(keyData.translationKey); - kb.setBoundKey(skinKey); - mergedList.add(kb); + // 1) Key exists in client - add it to output + String translationKey = boundKey.getTranslationKey(); + mergedMap.put(id, new KeyData(translationKey, id)); + + // Remove from remaining skin keybinds since we found it in client + remainingSkinKeybinds.remove(id); + } else { + // 2) Key exists but is unbound - remove it from skin keybinds + remainingSkinKeybinds.remove(id); } } - return mergedList; + + // 3) Add all leftover items from skinKeybindMap to output + mergedMap.putAll(remainingSkinKeybinds); + + return mergedMap; } /** @@ -302,7 +308,6 @@ public static int applyKeybinds(Map skinKeybindMap) { String id = kb.getTranslationKey(); if (skinKeybindMap.containsKey(id)) { - System.out.println("ID Found: " + id); KeyData keyData = skinKeybindMap.get(id); // Parse the key from its translation key diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java index 2935d0d..3df9c68 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java +++ b/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java @@ -59,7 +59,7 @@ protected void init() { SkinKeybindManagerClient.decodePlayerSkin(skin, variant); // Merge with current client bindings (client takes priority) - List mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); // Encode merged bindings into skin BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( @@ -130,7 +130,7 @@ protected void init() { // Decode skin keybinds and merge with client Map skinKeybinds = SkinKeybindManagerClient.decodePlayerSkin(skin, variant); - List mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); // Encode and save BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( From 2b338f4bdae0eb21da61d14025e2d45d29e2fca3 Mon Sep 17 00:00:00 2001 From: TheDuckMan64 Date: Fri, 7 Nov 2025 18:30:58 +1100 Subject: [PATCH 09/10] Refactor --- .../SkinKeybindManagerClient.java | 480 ------------------ .../SkinKeybindManagerClient.java | 207 ++++++++ .../SkinKeybindManagerScreen.java | 38 +- .../skinkeybindmanager/SkinKeybindUtils.java | 321 ++++++++++++ .../SkinKeybindManager.java | 2 +- .../mixin/ExampleMixin.java | 2 +- src/main/resources/fabric.mod.json | 4 +- 7 files changed, 558 insertions(+), 496 deletions(-) delete mode 100644 src/client/java/mod/theduckman64/SkinKeybindManagerClient.java create mode 100644 src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerClient.java rename src/client/java/{mod/theduckman64 => theduckman64/skinkeybindmanager}/SkinKeybindManagerScreen.java (81%) create mode 100644 src/client/java/theduckman64/skinkeybindmanager/SkinKeybindUtils.java rename src/main/java/{mod/theduckman64 => theduckman64/skinkeybindmanager}/SkinKeybindManager.java (94%) rename src/main/java/{mod/theduckman64 => theduckman64/skinkeybindmanager}/mixin/ExampleMixin.java (91%) diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java b/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java deleted file mode 100644 index 39d395e..0000000 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerClient.java +++ /dev/null @@ -1,480 +0,0 @@ -package mod.theduckman64; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; -import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; -import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; -import net.fabricmc.fabric.api.client.screen.v1.Screens; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.option.ControlsOptionsScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.option.KeyBinding; -import net.minecraft.client.session.Session; -import net.minecraft.client.toast.SystemToast; -import net.minecraft.client.util.InputUtil; -import net.minecraft.text.Text; -import okhttp3.*; -import org.jetbrains.annotations.NotNull; -import org.tukaani.xz.LZMA2Options; -import org.tukaani.xz.XZInputStream; -import org.tukaani.xz.XZOutputStream; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.*; -import java.util.*; - -public class SkinKeybindManagerClient implements ClientModInitializer { - - private static final OkHttpClient client = new OkHttpClient(); - private static boolean ran = false; - public static Session session; - - private static final int[][] OVERLAY_RECTS_CLASSIC = { - {0, 0, 8, 8}, - {24, 0, 40, 8}, - {56, 0, 64, 8}, - {0, 16, 4, 20}, - {12, 16, 20, 20}, - {36, 16, 44, 20}, - {52, 16, 56, 20}, - {0, 32, 4, 36}, - {12, 32, 20, 36}, - {36, 32, 44, 36}, - {52, 32, 56, 36}, - {0, 48, 4, 52}, - {12, 48, 20, 52}, - {28, 48, 36, 52}, - {44, 48, 52, 52}, - {60, 48, 64, 52}, - {56, 16, 64, 48} - }; - - private static final int[][] OVERLAY_RECTS_SLIM = { - {0, 0, 8, 8}, - {24, 0, 40, 8}, - {56, 0, 64, 8}, - {0, 16, 4, 20}, - {12, 16, 20, 20}, - {36, 16, 44, 20}, - {50, 16, 54, 20}, - {0, 32, 4, 36}, - {12, 32, 20, 36}, - {36, 32, 44, 36}, - {50, 32, 54, 36}, - {0, 48, 4, 52}, - {12, 48, 20, 52}, - {28, 48, 36, 52}, - {42, 48, 52, 52}, - {46, 52, 48, 64}, - {58, 48, 64, 52}, - {54, 16, 64, 48}, - {62, 52, 64, 64} - }; - - private static int[][] getOverlayRects(String variant) { - return variant.equalsIgnoreCase("slim") ? OVERLAY_RECTS_SLIM : OVERLAY_RECTS_CLASSIC; - } - - @Override - public void onInitializeClient() { - ClientTickEvents.END_CLIENT_TICK.register(mc -> { - if (ran) return; - session = mc.getSession(); - ran = true; - }); - - ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - if (!(screen instanceof ControlsOptionsScreen)) return; - - ButtonWidget myButton = ButtonWidget.builder( - Text.literal("Skin Keybind Manager"), - b -> client.setScreen(new SkinKeybindManagerScreen(screen)) - ) - .dimensions(5, 5, 150, 20) - .build(); - - Screens.getButtons(screen).add(myButton); - }); - } - - public static BufferedImage encodePlayerSkin(Map keybindMap, BufferedImage skin, String variant) throws Exception { - if (skin == null) return null; - - // Step 1: Build keybind string (ID:KEY:TYPE format where TYPE is K or M) - StringBuilder sb = new StringBuilder(); - sb.append("#SKINKEYBINDS_START\n"); - if (keybindMap != null && !keybindMap.isEmpty()) { - for (Map.Entry entry : keybindMap.entrySet()) { - String id = entry.getKey(); - KeyData keyData = entry.getValue(); - sb.append(id).append(":").append(keyData.translationKey).append(";"); - } - } - sb.append("\n#SKINKEYBINDS_END\n"); - - // Step 2: Compress with LZMA/XZ - byte[] compressed; - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - XZOutputStream xzOut = new XZOutputStream(baos, new LZMA2Options())) { - xzOut.write(sb.toString().getBytes()); - xzOut.finish(); - compressed = baos.toByteArray(); - } - - // Step 2.5: Prepare data with a 4-byte length prefix - ByteArrayOutputStream finalDataOut = new ByteArrayOutputStream(); - finalDataOut.write((compressed.length >> 24) & 0xFF); - finalDataOut.write((compressed.length >> 16) & 0xFF); - finalDataOut.write((compressed.length >> 8) & 0xFF); - finalDataOut.write(compressed.length & 0xFF); - finalDataOut.write(compressed); - byte[] prefixedCompressed = finalDataOut.toByteArray(); - - // Step 3: Write bytes into overlay pixels (ARGB format) - int[][] overlayRects = getOverlayRects(variant); - int idx = 0; - for (int[] rect : overlayRects) { - for (int y = rect[1]; y < rect[3]; y++) { - for (int x = rect[0]; x < rect[2]; x++) { - if (idx + 3 < prefixedCompressed.length) { - int a = prefixedCompressed[idx++] & 0xFF; - int r = prefixedCompressed[idx++] & 0xFF; - int g = prefixedCompressed[idx++] & 0xFF; - int b = prefixedCompressed[idx++] & 0xFF; - int argb = (a << 24) | (r << 16) | (g << 8) | b; - skin.setRGB(x, y, argb); - } else { - skin.setRGB(x, y, 0x00000000); - } - } - } - } - - return skin; - } - - /** - * Merge existing skin keybinds with current Fabric keybinds. - * Returns a map of keybind ID -> KeyData with the following logic: - * 1) If the key exists (is bound) in the client, add it to the output - * 2) If the key exists but is unbound in the client, remove it from skinKeybindMap - * 3) Add all leftover items in skinKeybindMap to the output - */ - public static Map getMergedKeybinds(Map skinKeybindMap) { - MinecraftClient mc = MinecraftClient.getInstance(); - Map mergedMap = new HashMap<>(); - - // Create a copy of skinKeybindMap to track leftovers - Map remainingSkinKeybinds = skinKeybindMap != null - ? new HashMap<>(skinKeybindMap) - : new HashMap<>(); - - for (KeyBinding kb : mc.options.allKeys) { - String id = kb.getTranslationKey(); - InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); - - // Check if key is bound (not UNKNOWN_KEY and code != -1) - if (boundKey != InputUtil.UNKNOWN_KEY && boundKey.getCode() != -1) { - // 1) Key exists in client - add it to output - String translationKey = boundKey.getTranslationKey(); - mergedMap.put(id, new KeyData(translationKey, id)); - - // Remove from remaining skin keybinds since we found it in client - remainingSkinKeybinds.remove(id); - } else { - // 2) Key exists but is unbound - remove it from skin keybinds - remainingSkinKeybinds.remove(id); - } - } - - // 3) Add all leftover items from skinKeybindMap to output - mergedMap.putAll(remainingSkinKeybinds); - - return mergedMap; - } - - /** - * Decode keybinds from a skin image overlay. - * Returns a map of keybind ID -> KeyData (key translation + type) for later application. - */ - public static Map decodePlayerSkin(BufferedImage skin, String variant) { - Map keybindMap = new HashMap<>(); - if (skin == null) return keybindMap; - - int[][] overlayRects = getOverlayRects(variant); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // Step 1: Extract overlay bytes (ARGB format) - for (int[] rect : overlayRects) { - for (int y = rect[1]; y < rect[3]; y++) { - for (int x = rect[0]; x < rect[2]; x++) { - int argb = skin.getRGB(x, y); - baos.write((argb >> 24) & 0xFF); // Alpha - baos.write((argb >> 16) & 0xFF); // Red - baos.write((argb >> 8) & 0xFF); // Green - baos.write(argb & 0xFF); // Blue - } - } - } - - byte[] bytes = baos.toByteArray(); - if (bytes.length < 4) return keybindMap; - - // Step 2: Read the 4-byte length prefix - int dataLength = ((bytes[0] & 0xFF) << 24) | - ((bytes[1] & 0xFF) << 16) | - ((bytes[2] & 0xFF) << 8) | - (bytes[3] & 0xFF); - - if (dataLength <= 0 || dataLength > bytes.length - 4) { - showToast("No prior skin data found"); - return keybindMap; - } - - byte[] xzData = Arrays.copyOfRange(bytes, 4, 4 + dataLength); - - // Step 3: Decompress XZ - try (XZInputStream xzIn = new XZInputStream(new ByteArrayInputStream(xzData))) { - ByteArrayOutputStream decoded = new ByteArrayOutputStream(); - byte[] buffer = new byte[8192]; - int n; - while ((n = xzIn.read(buffer)) != -1) decoded.write(buffer, 0, n); - - String result = decoded.toString(); - int start = result.indexOf("#SKINKEYBINDS_START"); - int end = result.indexOf("#SKINKEYBINDS_END"); - if (start == -1 || end == -1 || start >= end) return keybindMap; - - String data = result.substring(start + "#SKINKEYBINDS_START".length(), end).trim(); - if (!data.isEmpty()) { - String[] entries = data.split(";"); - for (String entry : entries) { - if (entry.isEmpty()) continue; - String[] parts = entry.split(":"); - if (parts.length == 2) { - String id = parts[0]; - String translationKey = parts[1]; - keybindMap.put(id, new KeyData(translationKey, id)); - } - } - } - } catch (Exception ignored) {} - return keybindMap; - } - - /** - * Simple data class to hold key translation and type - */ - public static class KeyData { - public final String translationKey; - public final String id; - - public KeyData(String keyTranslationKey, String id) { - this.translationKey = keyTranslationKey; - this.id = id; - } - } - - public static void showToast(String message) { - MinecraftClient.getInstance().getToastManager().add( - SystemToast.create( - MinecraftClient.getInstance(), - SystemToast.Type.NARRATOR_TOGGLE, - Text.literal("Skin Keybind Manager"), - Text.literal(message) - ) - ); - } - - /** - * Apply keybinds from a decoded map (translationKey -> KeyData). - * Matches keybinds by translation key with existing client keybinds. - * Always overwrites existing bindings with the skin's bindings. - */ - public static int applyKeybinds(Map skinKeybindMap) { - if (skinKeybindMap == null || skinKeybindMap.isEmpty()) return 0; - - MinecraftClient mc = MinecraftClient.getInstance(); - KeyBinding[] allKeys = mc.options.allKeys; - int applied = 0; - - for (KeyBinding kb : allKeys) { - String id = kb.getTranslationKey(); - - if (skinKeybindMap.containsKey(id)) { - KeyData keyData = skinKeybindMap.get(id); - - // Parse the key from its translation key - InputUtil.Key newKey = InputUtil.fromTranslationKey(keyData.translationKey); - - kb.setBoundKey(newKey); - applied++; - } - } - - KeyBinding.updateKeysByCode(); - return applied; - } - - public static @NotNull File saveSkinToDisk(BufferedImage skin) throws IOException { - File out = new File(FabricLoader.getInstance().getGameDir() + "/" + "skin_encoded.png"); - ImageIO.write(skin, "PNG", out); - return out; - } - - public static BufferedImage loadSkinFromDisk() { - try { - File skinFile = new File(FabricLoader.getInstance().getGameDir() + "/" + "skin_encoded.png"); - if (!skinFile.exists()) { - showToast("No skin on disk"); - return null; - } - return ImageIO.read(skinFile); - } catch (Exception e) { - showToast("Error: " + e.getMessage()); - e.printStackTrace(); - return null; - } - } - - public static BufferedImage downloadSkin(String username) throws Exception { - String uuid = getProfileUUID(username); - if (uuid == null) { - showToast("Error: UUID not found"); - } - - String accessToken = session.getAccessToken(); - String response = getSessionServerProfile(uuid, accessToken); - JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); - JsonArray properties = profileJson.getAsJsonArray("properties"); - - String base64Value = null; - for (JsonElement e : properties) { - JsonObject prop = e.getAsJsonObject(); - if ("textures".equals(prop.get("name").getAsString())) { - base64Value = prop.get("value").getAsString(); - break; - } - } - - if (base64Value == null) { - return null; - } - - String decodedJson = new String(java.util.Base64.getDecoder().decode(base64Value)); - JsonObject texturesJson = JsonParser.parseString(decodedJson).getAsJsonObject().getAsJsonObject("textures"); - if (!texturesJson.has("SKIN")) return null; - - String skinUrl = texturesJson.getAsJsonObject("SKIN").get("url").getAsString(); - - Request request = new Request.Builder().url(skinUrl).build(); - try (Response resp = client.newCall(request).execute()) { - if (!resp.isSuccessful()) { - showToast("Failed to download skin: " + resp.code()); - return null; - } - byte[] imageBytes; - assert resp.body() != null; - imageBytes = resp.body().bytes(); - return ImageIO.read(new ByteArrayInputStream(imageBytes)); - } - } - - public static String getVariant(String username) throws IOException { - String uuid = getProfileUUID(username); - String accessToken = session.getAccessToken(); - - // Get the profile JSON from Mojang session server - String response = getSessionServerProfile(uuid, accessToken); - JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); - JsonArray properties = profileJson.getAsJsonArray("properties"); - - String base64Value = null; - for (JsonElement e : properties) { - JsonObject prop = e.getAsJsonObject(); - if ("textures".equals(prop.get("name").getAsString())) { - base64Value = prop.get("value").getAsString(); - break; - } - } - - if (base64Value == null) { - System.err.println("[SkinFetcher] No 'textures' property found for user: " + username); - return "classic"; // default fallback - } - - // Decode the Base64 value - String decodedJson = new String(Base64.getDecoder().decode(base64Value)); - JsonObject texturesJson = JsonParser.parseString(decodedJson) - .getAsJsonObject() - .getAsJsonObject("textures"); - - if (!texturesJson.has("SKIN")) return "classic"; - - JsonObject skinObject = texturesJson.getAsJsonObject("SKIN"); - - // Return the model variant if present, otherwise "classic" - return skinObject.has("metadata") && skinObject.getAsJsonObject("metadata").has("model") - ? skinObject.getAsJsonObject("metadata").get("model").getAsString() - : "classic"; - } - - public static boolean uploadSkin(File skinFile, String slim) throws IOException { - MinecraftClient mc = MinecraftClient.getInstance(); - Session session = mc.getSession(); - String accessToken = session.getAccessToken(); - - String url = "https://api.minecraftservices.com/minecraft/profile/skins"; - - MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); - bodyBuilder.addFormDataPart("file", skinFile.getName(), - RequestBody.create(skinFile, MediaType.parse("image/png"))); - bodyBuilder.addFormDataPart("variant", slim); - - Request request = new Request.Builder() - .url(url) - .post(bodyBuilder.build()) - .addHeader("Authorization", "Bearer " + accessToken) - .build(); - - try (Response resp = client.newCall(request).execute()) { - if (!resp.isSuccessful()) { - showToast("Failed to upload skin: " + resp.code() + " " + resp.message()); - return false; - } - return true; - } - } - - private static String getProfileUUID(String username) throws IOException { - String url = "https://api.mojang.com/users/profiles/minecraft/" + username; - Request request = new Request.Builder().url(url).build(); - try (Response resp = client.newCall(request).execute()) { - if (!resp.isSuccessful()) return null; - assert resp.body() != null; - String body = resp.body().string(); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - return json.get("id").getAsString(); - } - } - - private static String getSessionServerProfile(String uuid, String accessToken) throws IOException { - Request request = new Request.Builder() - .url("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid) - .addHeader("Authorization", "Bearer " + accessToken) - .build(); - - try (Response resp = client.newCall(request).execute()) { - if (!resp.isSuccessful()) - throw new IOException("[SkinKeybindManager] Session server HTTP error: " + resp); - assert resp.body() != null; - return resp.body().string(); - } - } -} diff --git a/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerClient.java b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerClient.java new file mode 100644 index 0000000..1e025dc --- /dev/null +++ b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerClient.java @@ -0,0 +1,207 @@ +package theduckman64.skinkeybindmanager; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.option.ControlsOptionsScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.session.Session; +import net.minecraft.client.toast.SystemToast; +import net.minecraft.client.util.InputUtil; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SkinKeybindManagerClient implements ClientModInitializer { + + private static boolean ran = false; + public static Session session; + + @Override + public void onInitializeClient() { + ClientTickEvents.END_CLIENT_TICK.register(mc -> { + if (ran) return; + session = mc.getSession(); + ran = true; + }); + + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (!(screen instanceof ControlsOptionsScreen)) return; + + ButtonWidget myButton = ButtonWidget.builder( + Text.literal("Skin Keybind Manager"), + b -> client.setScreen(new SkinKeybindManagerScreen(screen)) // Assumes this screen exists + ) + .dimensions(5, 5, 150, 20) + .build(); + + Screens.getButtons(screen).add(myButton); + }); + } + + /** + * Merge existing skin keybinds with current Fabric keybinds. + * Returns a map of keybind ID -> KeyData with the following logic: + * 1) If the key exists (is bound) in the client, add it to the output + * 2) If the key exists but is unbound in the client, remove it from skinKeybindMap + * 3) Add all leftover items in skinKeybindMap to the output + */ + public static Map getMergedKeybinds(Map skinKeybindMap) { + MinecraftClient mc = MinecraftClient.getInstance(); + Map mergedMap = new HashMap<>(); + + // Create a copy of skinKeybindMap to track leftovers + Map remainingSkinKeybinds = skinKeybindMap != null + ? new HashMap<>(skinKeybindMap) + : new HashMap<>(); + + for (KeyBinding kb : mc.options.allKeys) { + String id = kb.getTranslationKey(); + InputUtil.Key boundKey = KeyBindingHelper.getBoundKeyOf(kb); + + // Check if key is bound (not UNKNOWN_KEY and code != -1) + if (boundKey != InputUtil.UNKNOWN_KEY && boundKey.getCode() != -1) { + // 1) Key exists in client - add it to output + String translationKey = boundKey.getTranslationKey(); + mergedMap.put(id, new SkinKeybindUtils.KeyData(translationKey, id)); + + // Remove from remaining skin keybinds since we found it in client + remainingSkinKeybinds.remove(id); + } else { + // 2) Key exists but is unbound - remove it from skin keybinds + remainingSkinKeybinds.remove(id); + } + } + + // 3) Add all leftover items from skinKeybindMap to output + mergedMap.putAll(remainingSkinKeybinds); + + return mergedMap; + } + + /** + * Apply keybinds from a decoded map (translationKey -> KeyData). + * Matches keybinds by translation key with existing client keybinds. + * Always overwrites existing bindings with the skin's bindings. + */ + public static int applyKeybinds(Map skinKeybindMap) { + if (skinKeybindMap == null || skinKeybindMap.isEmpty()) return 0; + + MinecraftClient mc = MinecraftClient.getInstance(); + KeyBinding[] allKeys = mc.options.allKeys; + int applied = 0; + + for (KeyBinding kb : allKeys) { + String id = kb.getTranslationKey(); + + if (skinKeybindMap.containsKey(id)) { + SkinKeybindUtils.KeyData keyData = skinKeybindMap.get(id); + + // Parse the key from its translation key + InputUtil.Key newKey = InputUtil.fromTranslationKey(keyData.translationKey()); + + kb.setBoundKey(newKey); + applied++; + } + } + + KeyBinding.updateKeysByCode(); + return applied; + } + + public static void showToast(String message) { + MinecraftClient.getInstance().getToastManager().add( + SystemToast.create( + MinecraftClient.getInstance(), + SystemToast.Type.NARRATOR_TOGGLE, + Text.literal("Skin Keybind Manager"), + Text.literal(message) + ) + ); + } + + // --- Wrapper Methods --- + + private static File getSkinFile() { + return new File(FabricLoader.getInstance().getGameDir() + "/" + "skin_encoded.png"); + } + + public static @NotNull File saveSkinToDisk(BufferedImage skin) throws IOException { + return SkinKeybindUtils.saveSkinToDisk(skin, getSkinFile()); + } + + public static BufferedImage loadSkinFromDisk() { + try { + File skinFile = getSkinFile(); + if (!skinFile.exists()) { + showToast("No skin on disk"); + return null; + } + return SkinKeybindUtils.loadSkinFromDisk(skinFile); + } catch (Exception e) { + showToast("Error: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + public static BufferedImage downloadSkin(String username) { + if (session == null) { + showToast("Session not initialized!"); + return null; + } + try { + return SkinKeybindUtils.downloadSkin(username, session.getAccessToken()); + } catch (Exception e) { + showToast("Failed to download skin: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + public static String getVariant(String username) { + if (session == null) { + showToast("Session not initialized!"); + return "classic"; + } + try { + return SkinKeybindUtils.getVariant(username, session.getAccessToken()); + } catch (Exception e) { + showToast("Failed to get variant: " + e.getMessage()); + e.printStackTrace(); + return "classic"; + } + } + + public static boolean uploadSkin(File skinFile, String slim) { + if (session == null) { + showToast("Session not initialized!"); + return false; + } + try { + return SkinKeybindUtils.uploadSkin(skinFile, slim, session.getAccessToken()); + } catch (IOException e) { + showToast("Failed to upload skin: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + public static Map decodePlayerSkin(BufferedImage skin, String variant) { + Map map = SkinKeybindUtils.decodePlayerSkin(skin, variant); + if (map.isEmpty()) { + showToast("No prior skin data found"); + } + return map; + } +} \ No newline at end of file diff --git a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java similarity index 81% rename from src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java rename to src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java index 3df9c68..9c9636f 100644 --- a/src/client/java/mod/theduckman64/SkinKeybindManagerScreen.java +++ b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java @@ -1,15 +1,13 @@ -package mod.theduckman64; +package theduckman64.skinkeybindmanager; import net.fabricmc.fabric.api.client.screen.v1.Screens; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.option.KeyBinding; import net.minecraft.text.Text; import net.minecraft.client.toast.SystemToast; import java.awt.image.BufferedImage; -import java.util.List; import java.util.Map; public class SkinKeybindManagerScreen extends Screen { @@ -55,14 +53,16 @@ protected void init() { ); // Decode existing skin keybinds - Map skinKeybinds = + // --- Use SkinKeybindUtils.KeyData --- + Map skinKeybinds = SkinKeybindManagerClient.decodePlayerSkin(skin, variant); // Merge with current client bindings (client takes priority) - Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + // --- Use SkinKeybindUtils.KeyData --- + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); // Encode merged bindings into skin - BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( + BufferedImage encodedSkin = SkinKeybindUtils.encodePlayerSkin( // This can call SkinKeybindUtils directly mergedBindings, skin, variant); @@ -90,11 +90,22 @@ protected void init() { Text.of("Download keybinds from skin"), b -> { try { + long currentTime = System.currentTimeMillis(); + long timeSinceLastUpload = currentTime - lastUploadTime; + + if (timeSinceLastUpload < UPLOAD_COOLDOWN_MS) { + long remainingMs = UPLOAD_COOLDOWN_MS - timeSinceLastUpload; + long remainingSeconds = (remainingMs + 999) / 1000; // Round up + showToast("Please wait " + remainingSeconds + " seconds (API Limit reached)"); + return; + } + BufferedImage skin = SkinKeybindManagerClient.downloadSkin( SkinKeybindManagerClient.session.getUsername() ); - Map keybinds = + // --- Use SkinKeybindUtils.KeyData --- + Map keybinds = SkinKeybindManagerClient.decodePlayerSkin( skin, SkinKeybindManagerClient.getVariant( @@ -103,7 +114,7 @@ protected void init() { ); int applied = SkinKeybindManagerClient.applyKeybinds(keybinds); - + lastUploadTime = System.currentTimeMillis(); showToast("Applied " + applied + " keybinds from skin."); } catch (Exception e) { showToast("Error: " + e.getMessage()); @@ -128,12 +139,14 @@ protected void init() { ); // Decode skin keybinds and merge with client - Map skinKeybinds = + // --- Use SkinKeybindUtils.KeyData --- + Map skinKeybinds = SkinKeybindManagerClient.decodePlayerSkin(skin, variant); - Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + // --- Use SkinKeybindUtils.KeyData --- + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); // Encode and save - BufferedImage encodedSkin = SkinKeybindManagerClient.encodePlayerSkin( + BufferedImage encodedSkin = SkinKeybindUtils.encodePlayerSkin( // This can call SkinKeybindUtils directly mergedBindings, skin, variant @@ -160,7 +173,8 @@ protected void init() { return; } - Map decoded = + // --- Use SkinKeybindUtils.KeyData --- + Map decoded = SkinKeybindManagerClient.decodePlayerSkin( skin, SkinKeybindManagerClient.getVariant( diff --git a/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindUtils.java b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindUtils.java new file mode 100644 index 0000000..3ddc877 --- /dev/null +++ b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindUtils.java @@ -0,0 +1,321 @@ +package theduckman64.skinkeybindmanager; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; +import org.tukaani.xz.LZMA2Options; +import org.tukaani.xz.XZInputStream; +import org.tukaani.xz.XZOutputStream; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * Contains all Minecraft-version-agnostic logic for handling skin keybinds. + * This class does not depend on any Minecraft or Fabric APIs. + */ +public class SkinKeybindUtils { + + public static final OkHttpClient client = new OkHttpClient(); + + private static final int[][] OVERLAY_RECTS_CLASSIC = { + {0, 0, 8, 8}, {24, 0, 40, 8}, {56, 0, 64, 8}, + {0, 16, 4, 20}, {12, 16, 20, 20}, {36, 16, 44, 20}, + {52, 16, 56, 20}, {0, 32, 4, 36}, {12, 32, 20, 36}, + {36, 32, 44, 36}, {52, 32, 56, 36}, {0, 48, 4, 52}, + {12, 48, 20, 52}, {28, 48, 36, 52}, {44, 48, 52, 52}, + {60, 48, 64, 52}, {56, 16, 64, 48} + }; + + private static final int[][] OVERLAY_RECTS_SLIM = { + {0, 0, 8, 8}, {24, 0, 40, 8}, {56, 0, 64, 8}, + {0, 16, 4, 20}, {12, 16, 20, 20}, {36, 16, 44, 20}, + {50, 16, 54, 20}, {0, 32, 4, 36}, {12, 32, 20, 36}, + {36, 32, 44, 36}, {50, 32, 54, 36}, {0, 48, 4, 52}, + {12, 48, 20, 52}, {28, 48, 36, 52}, {42, 48, 52, 52}, + {46, 52, 48, 64}, {58, 48, 64, 52}, {54, 16, 64, 48}, + {62, 52, 64, 64} + }; + + /** + * Simple data class to hold key translation and type + */ + public record KeyData(String translationKey, String id) { + } + + private static int[][] getOverlayRects(String variant) { + return variant.equalsIgnoreCase("slim") ? OVERLAY_RECTS_SLIM : OVERLAY_RECTS_CLASSIC; + } + + public static BufferedImage encodePlayerSkin(Map keybindMap, BufferedImage skin, String variant) throws IOException { + if (skin == null) return null; + + // Step 1: Build keybind string + StringBuilder sb = new StringBuilder(); + sb.append("#SKINKEYBINDS_START\n"); + if (keybindMap != null && !keybindMap.isEmpty()) { + for (Map.Entry entry : keybindMap.entrySet()) { + String id = entry.getKey(); + KeyData keyData = entry.getValue(); + sb.append(id).append(":").append(keyData.translationKey).append(";"); + } + } + sb.append("\n#SKINKEYBINDS_END\n"); + + // Step 2: Compress with LZMA/XZ + byte[] compressed; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XZOutputStream xzOut = new XZOutputStream(baos, new LZMA2Options())) { + xzOut.write(sb.toString().getBytes()); + xzOut.finish(); + compressed = baos.toByteArray(); + } + + // Step 2.5: Prepare data with a 4-byte length prefix + ByteArrayOutputStream finalDataOut = new ByteArrayOutputStream(); + finalDataOut.write((compressed.length >> 24) & 0xFF); + finalDataOut.write((compressed.length >> 16) & 0xFF); + finalDataOut.write((compressed.length >> 8) & 0xFF); + finalDataOut.write(compressed.length & 0xFF); + finalDataOut.write(compressed); + byte[] prefixedCompressed = finalDataOut.toByteArray(); + + // Step 3: Write bytes into overlay pixels (ARGB format) + int[][] overlayRects = getOverlayRects(variant); + int idx = 0; + for (int[] rect : overlayRects) { + for (int y = rect[1]; y < rect[3]; y++) { + for (int x = rect[0]; x < rect[2]; x++) { + if (idx + 3 < prefixedCompressed.length) { + int a = prefixedCompressed[idx++] & 0xFF; + int r = prefixedCompressed[idx++] & 0xFF; + int g = prefixedCompressed[idx++] & 0xFF; + int b = prefixedCompressed[idx++] & 0xFF; + int argb = (a << 24) | (r << 16) | (g << 8) | b; + skin.setRGB(x, y, argb); + } else { + skin.setRGB(x, y, 0x00000000); // Clear remaining pixels + } + } + } + } + + return skin; + } + + /** + * Decode keybinds from a skin image overlay. + * Returns a map of keybind ID -> KeyData (key translation + type) for later application. + */ + public static Map decodePlayerSkin(BufferedImage skin, String variant) { + Map keybindMap = new HashMap<>(); + if (skin == null) return keybindMap; + + int[][] overlayRects = getOverlayRects(variant); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // Step 1: Extract overlay bytes (ARGB format) + for (int[] rect : overlayRects) { + for (int y = rect[1]; y < rect[3]; y++) { + for (int x = rect[0]; x < rect[2]; x++) { + int argb = skin.getRGB(x, y); + baos.write((argb >> 24) & 0xFF); // Alpha + baos.write((argb >> 16) & 0xFF); // Red + baos.write((argb >> 8) & 0xFF); // Green + baos.write(argb & 0xFF); // Blue + } + } + } + + byte[] bytes = baos.toByteArray(); + if (bytes.length < 4) return keybindMap; + + // Step 2: Read the 4-byte length prefix + int dataLength = ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + + if (dataLength <= 0 || dataLength > bytes.length - 4) { + // No data found or data is corrupt + return keybindMap; + } + + byte[] xzData = Arrays.copyOfRange(bytes, 4, 4 + dataLength); + + // Step 3: Decompress XZ + try (XZInputStream xzIn = new XZInputStream(new ByteArrayInputStream(xzData))) { + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int n; + while ((n = xzIn.read(buffer)) != -1) decoded.write(buffer, 0, n); + + String result = decoded.toString(); + int start = result.indexOf("#SKINKEYBINDS_START"); + int end = result.indexOf("#SKINKEYBINDS_END"); + if (start == -1 || end == -1 || start >= end) return keybindMap; + + String data = result.substring(start + "#SKINKEYBINDS_START".length(), end).trim(); + if (!data.isEmpty()) { + String[] entries = data.split(";"); + for (String entry : entries) { + if (entry.isEmpty()) continue; + String[] parts = entry.split(":"); + if (parts.length == 2) { + String id = parts[0]; + String translationKey = parts[1]; + keybindMap.put(id, new KeyData(translationKey, id)); + } + } + } + } catch (Exception ignored) { + // Decompression failed, return empty map + } + return keybindMap; + } + + public static @NotNull File saveSkinToDisk(BufferedImage skin, File outFile) throws IOException { + ImageIO.write(skin, "PNG", outFile); + return outFile; + } + + public static BufferedImage loadSkinFromDisk(File skinFile) throws IOException { + if (!skinFile.exists()) { + throw new FileNotFoundException("Skin file not found: " + skinFile.getAbsolutePath()); + } + return ImageIO.read(skinFile); + } + + public static BufferedImage downloadSkin(String username, String accessToken) throws Exception { + String uuid = getProfileUUID(username); + if (uuid == null) { + throw new IOException("UUID not found for user: " + username); + } + + String response = getSessionServerProfile(uuid, accessToken); + JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); + JsonArray properties = profileJson.getAsJsonArray("properties"); + + String base64Value = null; + for (JsonElement e : properties) { + JsonObject prop = e.getAsJsonObject(); + if ("textures".equals(prop.get("name").getAsString())) { + base64Value = prop.get("value").getAsString(); + break; + } + } + + if (base64Value == null) { + throw new IOException("No 'textures' property found for user."); + } + + String decodedJson = new String(Base64.getDecoder().decode(base64Value)); + JsonObject texturesJson = JsonParser.parseString(decodedJson).getAsJsonObject().getAsJsonObject("textures"); + if (!texturesJson.has("SKIN")) { + throw new IOException("Profile has no skin texture."); + } + + String skinUrl = texturesJson.getAsJsonObject("SKIN").get("url").getAsString(); + + Request request = new Request.Builder().url(skinUrl).build(); + try (Response resp = client.newCall(request).execute()) { + if (!resp.isSuccessful() || resp.body() == null) { + throw new IOException("Failed to download skin: " + resp.code() + " " + resp.message()); + } + byte[] imageBytes = resp.body().bytes(); + return ImageIO.read(new ByteArrayInputStream(imageBytes)); + } + } + + public static String getVariant(String username, String accessToken) throws IOException { + String uuid = getProfileUUID(username); + if (uuid == null) { + throw new IOException("UUID not found for user: " + username); + } + + String response = getSessionServerProfile(uuid, accessToken); + JsonObject profileJson = JsonParser.parseString(response).getAsJsonObject(); + JsonArray properties = profileJson.getAsJsonArray("properties"); + + String base64Value = null; + for (JsonElement e : properties) { + JsonObject prop = e.getAsJsonObject(); + if ("textures".equals(prop.get("name").getAsString())) { + base64Value = prop.get("value").getAsString(); + break; + } + } + + if (base64Value == null) { + System.err.println("[SkinFetcher] No 'textures' property found for user: " + username); + return "classic"; // default fallback + } + + String decodedJson = new String(Base64.getDecoder().decode(base64Value)); + JsonObject texturesJson = JsonParser.parseString(decodedJson) + .getAsJsonObject() + .getAsJsonObject("textures"); + + if (!texturesJson.has("SKIN")) return "classic"; + + JsonObject skinObject = texturesJson.getAsJsonObject("SKIN"); + + return skinObject.has("metadata") && skinObject.getAsJsonObject("metadata").has("model") + ? skinObject.getAsJsonObject("metadata").get("model").getAsString() + : "classic"; + } + + public static boolean uploadSkin(File skinFile, String slim, String accessToken) throws IOException { + String url = "https://api.minecraftservices.com/minecraft/profile/skins"; + + MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + bodyBuilder.addFormDataPart("file", skinFile.getName(), + RequestBody.create(skinFile, MediaType.parse("image/png"))); + bodyBuilder.addFormDataPart("variant", slim); + + Request request = new Request.Builder() + .url(url) + .post(bodyBuilder.build()) + .addHeader("Authorization", "Bearer " + accessToken) + .build(); + + try (Response resp = client.newCall(request).execute()) { + if (!resp.isSuccessful()) { + throw new IOException("Failed to upload skin: " + resp.code() + " " + resp.message()); + } + return true; + } + } + + private static String getProfileUUID(String username) throws IOException { + String url = "https://api.mojang.com/users/profiles/minecraft/" + username; + Request request = new Request.Builder().url(url).build(); + try (Response resp = client.newCall(request).execute()) { + if (!resp.isSuccessful() || resp.body() == null) return null; + String body = resp.body().string(); + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + return json.get("id").getAsString(); + } + } + + private static String getSessionServerProfile(String uuid, String accessToken) throws IOException { + Request request = new Request.Builder() + .url("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid) + .addHeader("Authorization", "Bearer " + accessToken) + .build(); + + try (Response resp = client.newCall(request).execute()) { + if (!resp.isSuccessful() || resp.body() == null) + throw new IOException("[SkinKeybindManager] Session server HTTP error: " + resp); + return resp.body().string(); + } + } +} diff --git a/src/main/java/mod/theduckman64/SkinKeybindManager.java b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java similarity index 94% rename from src/main/java/mod/theduckman64/SkinKeybindManager.java rename to src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java index ced09a7..c5acbca 100644 --- a/src/main/java/mod/theduckman64/SkinKeybindManager.java +++ b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java @@ -1,4 +1,4 @@ -package mod.theduckman64; +package theduckman64.skinkeybindmanager; import net.fabricmc.api.ModInitializer; diff --git a/src/main/java/mod/theduckman64/mixin/ExampleMixin.java b/src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java similarity index 91% rename from src/main/java/mod/theduckman64/mixin/ExampleMixin.java rename to src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java index f6e0fc5..0df705d 100644 --- a/src/main/java/mod/theduckman64/mixin/ExampleMixin.java +++ b/src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java @@ -1,4 +1,4 @@ -package mod.theduckman64.mixin; +package theduckman64.skinkeybindmanager.mixin; import net.minecraft.server.MinecraftServer; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index ffcf0e4..6de78eb 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -15,10 +15,10 @@ "environment": "*", "entrypoints": { "main": [ - "mod.theduckman64.SkinKeybindManager" + "theduckman64.skinkeybindmanager.SkinKeybindManager" ], "client": [ - "mod.theduckman64.SkinKeybindManagerClient" + "theduckman64.skinkeybindmanager.SkinKeybindManagerClient" ] }, "mixins": [ From ccec8906fa168f18eabc8ea677bdd239bc9debd6 Mon Sep 17 00:00:00 2001 From: TheDuckMan64 Date: Fri, 7 Nov 2025 18:31:57 +1100 Subject: [PATCH 10/10] Refactor --- .../skinkeybindmanager/SkinKeybindManager.java | 2 +- .../skinkeybindmanager/mixin/ExampleMixin.java | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java diff --git a/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java index c5acbca..ddc4715 100644 --- a/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java +++ b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java @@ -19,6 +19,6 @@ public void onInitialize() { // However, some things (like resources) may still be uninitialized. // Proceed with mild caution. - LOGGER.info("Hello Fabric world!"); + LOGGER.info("Client only mod!"); } } \ No newline at end of file diff --git a/src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java b/src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java deleted file mode 100644 index 0df705d..0000000 --- a/src/main/java/theduckman64/skinkeybindmanager/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package theduckman64.skinkeybindmanager.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadWorld") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadWorld()V - } -} \ No newline at end of file