diff --git a/build.gradle b/build.gradle index 62d5430..2a25972 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.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 ef49a93..de2dece 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.10 -yarn_mappings=1.21.10+build.2 + +minecraft_version=1.21 +yarn_mappings=1.21+build.9 loader_version=0.17.3 -loom_version=1.11-SNAPSHOT +loom_version=1.12-SNAPSHOT + +# Fabric API +fabric_version=0.102.0+1.21 # Mod Properties -mod_version=1.0.0 -maven_group=name.modid +mod_version=1.21 +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/name/modid/SkinKeybindManagerClient.java b/src/client/java/name/modid/SkinKeybindManagerClient.java deleted file mode 100644 index d02534a..0000000 --- a/src/client/java/name/modid/SkinKeybindManagerClient.java +++ /dev/null @@ -1,466 +0,0 @@ -package name.modid; - -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.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.util.InputUtil; -import net.minecraft.text.Text; -import okhttp3.*; -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 = { - {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 - {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 - }; - - 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 - {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 - }; - - 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; // run only once - - session = mc.getSession(); - ran = true; - }); - - 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"), - b -> client.setScreen(new SkinKeybindManagerScreen( - screen)) - ) - .dimensions(5, 5, 150, 20) - .build(); - - Screens.getButtons(screen).add(myButton); - }); - } - - public static BufferedImage encodePlayerSkin(List keybindings, BufferedImage skin, String variant) throws Exception { - if (skin == null) return null; - - // Step 1: Build keybind string - 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(";"); - } - } - 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(); - } - - // New Step 2.5: Prepare data with a 4-byte length prefix - // The length includes the XZ stream and its footer. - 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) - 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; - } - } - } - return skin; - } - - - - /** - * Merge existing skin keybinds with current Fabric keybinds. - * Existing skin keybinds take precedence if present, otherwise use current in-game bindings. - */ - 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 - MinecraftClient mc = MinecraftClient.getInstance(); - 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()); - } - } - - 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. - */ - public static List decodePlayerSkin(BufferedImage skin, String variant) { - List keybindings = new ArrayList<>(); - if (skin == null) return keybindings; - - int[][] overlayRects = getOverlayRects(variant); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - // Step 1: Extract overlay bytes - 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); - } - } - } - - byte[] bytes = baos.toByteArray(); - // Check if there's even space for the 4-byte prefix - if (bytes.length < 4) return keybindings; - - // 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); - - // 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; - } - - // 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 - 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 keybindings; - - 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 >= 3) { - 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)); - } - } - } - } catch (Exception e) { - System.out.println("[SkinKeybindManager] No keybinds found or decompression failed: " + e); - // Return empty list if nothing encoded yet - } - - return keybindings; - } - - - public static int applyKeybinds(List fromSkinBindings) { - if (fromSkinBindings == null) fromSkinBindings = List.of(); - - 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()); - applied++; - } else { - // Unbind the key - kb.setBoundKey(InputUtil.UNKNOWN_KEY); - } - } - - KeyBinding.updateKeysByCode(); - System.out.println("[SkinKeybindManager] Applied " + applied + " keybinds. Others unbound."); - return applied; - } - - - - - - public static void 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()); - } - - - 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()); - 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 null; - } - } - - public static BufferedImage downloadSkin(String username) throws Exception { - - // 1. Fetch UUID from Mojang API - String uuid = getProfileUUID(username); // your existing method - if (uuid == null) { - System.err.println("[SkinKeybindManager] Could not fetch UUID for username: " + username); - uuid = "fcab5be823974c298ff904911c72e294"; - //return null; - } - - // 2. Fetch session server profile to get skin URL - String accessToken = session.getAccessToken(); // your session object - String response = getSessionServerProfile(uuid, accessToken); // your existing method - 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("[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()); - return null; - } - byte[] 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 { - 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"; - - 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"); - } - - 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()) { - System.err.println("[SkinKeybindManager] 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; - 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); - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - System.out.println("JSON: " + json); - System.out.println("ID: " + json.get("id").getAsString()); - 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); - return resp.body().string(); - } - } -} 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/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/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java new file mode 100644 index 0000000..9c9636f --- /dev/null +++ b/src/client/java/theduckman64/skinkeybindmanager/SkinKeybindManagerScreen.java @@ -0,0 +1,213 @@ +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.text.Text; +import net.minecraft.client.toast.SystemToast; + +import java.awt.image.BufferedImage; +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( + SkinKeybindManagerClient.session.getUsername() + ); + + // Decode existing skin keybinds + // --- Use SkinKeybindUtils.KeyData --- + Map skinKeybinds = + SkinKeybindManagerClient.decodePlayerSkin(skin, variant); + + // Merge with current client bindings (client takes priority) + // --- Use SkinKeybindUtils.KeyData --- + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + + // Encode merged bindings into skin + BufferedImage encodedSkin = SkinKeybindUtils.encodePlayerSkin( // This can call SkinKeybindUtils directly + mergedBindings, + skin, + variant); + + boolean success = SkinKeybindManagerClient.uploadSkin( + SkinKeybindManagerClient.saveSkinToDisk(encodedSkin), + variant); + + if (success) { + showToast("Successfully uploaded keybinds to skin"); + lastUploadTime = System.currentTimeMillis(); + } 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 { + 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() + ); + + // --- Use SkinKeybindUtils.KeyData --- + Map keybinds = + SkinKeybindManagerClient.decodePlayerSkin( + skin, + SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ) + ); + + int applied = SkinKeybindManagerClient.applyKeybinds(keybinds); + lastUploadTime = System.currentTimeMillis(); + 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( + SkinKeybindManagerClient.session.getUsername() + ); + + // Decode skin keybinds and merge with client + // --- Use SkinKeybindUtils.KeyData --- + Map skinKeybinds = + SkinKeybindManagerClient.decodePlayerSkin(skin, variant); + // --- Use SkinKeybindUtils.KeyData --- + Map mergedBindings = SkinKeybindManagerClient.getMergedKeybinds(skinKeybinds); + + // Encode and save + BufferedImage encodedSkin = SkinKeybindUtils.encodePlayerSkin( // This can call SkinKeybindUtils directly + 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; + } + + // --- Use SkinKeybindUtils.KeyData --- + Map decoded = + SkinKeybindManagerClient.decodePlayerSkin( + skin, + SkinKeybindManagerClient.getVariant( + SkinKeybindManagerClient.session.getUsername() + ) + ); + 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/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/name/modid/mixin/ExampleMixin.java b/src/main/java/name/modid/mixin/ExampleMixin.java deleted file mode 100644 index bcb3971..0000000 --- a/src/main/java/name/modid/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package name.modid.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 diff --git a/src/main/java/name/modid/SkinKeybindManager.java b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java similarity index 90% rename from src/main/java/name/modid/SkinKeybindManager.java rename to src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java index a6c6e76..ddc4715 100644 --- a/src/main/java/name/modid/SkinKeybindManager.java +++ b/src/main/java/theduckman64/skinkeybindmanager/SkinKeybindManager.java @@ -1,4 +1,4 @@ -package name.modid; +package theduckman64.skinkeybindmanager; import net.fabricmc.api.ModInitializer; @@ -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/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index bc5e7e8..6de78eb 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,29 +3,29 @@ "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/Skin-Keybind-Manager" }, "license": "CC0-1.0", "icon": "assets/skin-keybind-manager/icon.png", "environment": "*", "entrypoints": { "main": [ - "name.modid.SkinKeybindManager" + "theduckman64.skinkeybindmanager.SkinKeybindManager" ], "client": [ - "name.modid.SkinKeybindManagerClient" + "theduckman64.skinkeybindmanager.SkinKeybindManagerClient" ] }, "mixins": [ ], "depends": { - "fabricloader": ">=0.17.3", - "minecraft": "~1.21.10", + "fabricloader": ">=0.15", + "minecraft": "1.21.*", "java": ">=21", "fabric-api": "*" },