diff --git a/src/main/java/meteordevelopment/meteorclient/systems/config/Config.java b/src/main/java/meteordevelopment/meteorclient/systems/config/Config.java index 64c157ea9c..0a39f6929e 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/config/Config.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/config/Config.java @@ -11,11 +11,13 @@ import meteordevelopment.meteorclient.settings.*; import meteordevelopment.meteorclient.systems.System; import meteordevelopment.meteorclient.systems.Systems; +import meteordevelopment.meteorclient.utils.misc.IconChanger; import meteordevelopment.meteorclient.utils.render.color.SettingColor; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.NbtList; import net.minecraft.nbt.NbtString; +import net.minecraft.util.Identifier; import java.util.ArrayList; import java.util.List; @@ -87,6 +89,14 @@ public class Config extends System { .build() ); + public final Setting customWindowIconIcon = sgVisual.add(new EnumSetting.Builder() + .name("custom-window-icon") + .description("The icon used as the window icon") + .defaultValue(Icons.Default) + .onChanged(value -> IconChanger.setIcon(value.identifier)) + .build() + ); + public final Setting friendColor = sgVisual.add(new ColorSetting.Builder() .name("friend-color") .description("The color used to show friends.") @@ -189,4 +199,17 @@ private List listFromTag(NbtCompound tag, String key) { for (NbtElement item : tag.getList(key, 8)) list.add(item.asString()); return list; } + + public enum Icons { + Meteor(MeteorClient.identifier("textures/meteor.png")), + Halloween(MeteorClient.identifier("textures/icons/windowicon/halloween.png")), + Christmas(MeteorClient.identifier("textures/icons/windowicon/christmas.png")), + Default(null); + + private final Identifier identifier; + + Icons(Identifier identifier) { + this.identifier = identifier; + } + } } diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/IconChanger.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/IconChanger.java new file mode 100644 index 0000000000..cf3d0c534e --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/IconChanger.java @@ -0,0 +1,119 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.misc; + +import meteordevelopment.meteorclient.MeteorClient; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import net.minecraft.SharedConstants; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.util.Icons; +import net.minecraft.resource.Resource; +import net.minecraft.util.Identifier; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWImage; +import org.lwjgl.stb.STBImage; +import org.lwjgl.system.MemoryStack; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import static meteordevelopment.meteorclient.MeteorClient.mc; + +public class IconChanger { + + public static void setIcon(Identifier iconPath) { + if (iconPath == null) { // If the default Minecraft icon should be used + try { + //Default Minecraft method for setting the windows' icon + mc.getWindow().setIcon( + mc.getDefaultResourcePack(), + SharedConstants.getGameVersion().isStable() ? Icons.RELEASE : Icons.SNAPSHOT + ); + } catch (IOException e) { + MeteorClient.LOG.error("Failed to set icon", e); + } + return; + } + + // Retrieve the native window handle of the Minecraft game window + long windowHandle = MinecraftClient.getInstance().getWindow().getHandle(); + setWindowIcon(windowHandle, iconPath); + } + + private static void setWindowIcon(long windowHandle, Identifier iconPath) { + try (MemoryStack stack = MemoryStack.stackPush()) { // Memory stack for temporary native allocations + // Create buffers to store width, height, and number of channels of the loaded image + IntBuffer w = stack.mallocInt(1); + IntBuffer h = stack.mallocInt(1); + IntBuffer channels = stack.mallocInt(1); + + // Load the icon image into memory + ByteBuffer icon = loadIcon(iconPath, w, h, channels); + if (icon != null) { + // Create GLFWImage objects to hold the icon's data + GLFWImage glfwImage1 = GLFWImage.malloc(); + glfwImage1.set(w.get(0), h.get(0), icon); // Set the image dimensions and data + GLFWImage glfwImage2 = GLFWImage.malloc(); + // Repeat for the second icon (We are doing this twice, because we need to set the icon for the taskbar and window bar) + glfwImage2.set(w.get(0), h.get(0), icon); + + // Create a buffer to hold multiple icons (for high DPI support) + GLFWImage.Buffer icons = GLFWImage.malloc(2); + icons.put(0, glfwImage1); // Add the first icon to the buffer + icons.put(1, glfwImage2); // Add the second icon to the buffer + + // Set the window icon using GLFW + GLFW.glfwSetWindowIcon(windowHandle, icons); + + // Free the allocated GLFWImage and buffer memory + icons.free(); + glfwImage1.free(); + glfwImage2.free(); + } else { + info("Failed to load icon: " + iconPath); + } + } + } + + private static ByteBuffer loadIcon(Identifier path, IntBuffer w, IntBuffer h, IntBuffer channels) { + try { + // Retrieve the resource from the game's resource manager using the provided path + Resource resource = mc.getResourceManager() + .getResource(path) + .orElseThrow(() -> new IOException("Icon not found: " + path)); + + // Open an input stream to read the icon file's raw data + InputStream inputStream = resource.getInputStream(); + + // Read all bytes from the input stream into a byte array + byte[] iconBytes = inputStream.readAllBytes(); + + // Create a direct ByteBuffer to store the raw icon data (necessary for native code) + ByteBuffer buffer = ByteBuffer.allocateDirect(iconBytes.length).put(iconBytes).flip(); + + // Load the image from the byte buffer using STBImage + ByteBuffer icon = STBImage.stbi_load_from_memory(buffer, w, h, channels, 4); // 4 = RGBA channels + + if (icon == null) { + // Log an error if the image could not be loaded + info("Failed to load image from memory for: " + path + " - " + STBImage.stbi_failure_reason()); + } + + return icon; // Return the loaded image + } catch (IOException e) { + // Handle IO exceptions during icon loading + MeteorClient.LOG.error("Failed to load icon", e); + return null; + } + } + + private static void info(String message) { + ChatUtils.forceNextPrefixClass(IconChanger.class); + ChatUtils.info(message); + } +} diff --git a/src/main/resources/assets/meteor-client/textures/icons/windowicon/christmas.png b/src/main/resources/assets/meteor-client/textures/icons/windowicon/christmas.png new file mode 100644 index 0000000000..f8ce064abb Binary files /dev/null and b/src/main/resources/assets/meteor-client/textures/icons/windowicon/christmas.png differ diff --git a/src/main/resources/assets/meteor-client/textures/icons/windowicon/halloween.png b/src/main/resources/assets/meteor-client/textures/icons/windowicon/halloween.png new file mode 100644 index 0000000000..b803823cb8 Binary files /dev/null and b/src/main/resources/assets/meteor-client/textures/icons/windowicon/halloween.png differ