Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public void init() {
register(ModuleInfosHud.INFO);
register(PotionTimersHud.INFO);
register(CombatHud.INFO);
register(ImageHud.INFO);

// Default config
if (isFirstInit) resetToDefaultElements();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import meteordevelopment.meteorclient.MeteorClient;
import meteordevelopment.meteorclient.events.meteor.CustomFontChangedEvent;
import meteordevelopment.meteorclient.gui.renderer.packer.TextureRegion;
import meteordevelopment.meteorclient.renderer.*;
import meteordevelopment.meteorclient.renderer.text.CustomTextRenderer;
import meteordevelopment.meteorclient.renderer.text.Font;
Expand Down Expand Up @@ -135,6 +136,18 @@ public void texture(Identifier id, double x, double y, double width, double heig
Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView());
}

public void texture(Identifier id, double x, double y, double width, double height, TextureRegion textureRegion, Color color, float scale) {
Renderer2D.TEXTURE.begin();
Renderer2D.TEXTURE.texQuad(x, y, width * scale, height * scale, textureRegion, color);
Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView());
}

public void texture(Identifier id, double x, double y, double width, double height, Color color, float scale) {
Renderer2D.TEXTURE.begin();
Renderer2D.TEXTURE.texQuad(x, y, width * scale, height * scale, color);
Renderer2D.TEXTURE.render(mc.getTextureManager().getTexture(id).getGlTextureView());
}

public double text(String text, double x, double y, Color color, boolean shadow, double scale) {
if (scale == -1) scale = hud.getTextScale();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.systems.hud.elements;

import meteordevelopment.meteorclient.gui.renderer.packer.TextureRegion;
import meteordevelopment.meteorclient.settings.DoubleSetting;
import meteordevelopment.meteorclient.settings.Setting;
import meteordevelopment.meteorclient.settings.SettingGroup;
import meteordevelopment.meteorclient.settings.StringSetting;
import meteordevelopment.meteorclient.systems.hud.Hud;
import meteordevelopment.meteorclient.systems.hud.HudElement;
import meteordevelopment.meteorclient.systems.hud.HudElementInfo;
import meteordevelopment.meteorclient.systems.hud.HudRenderer;
import meteordevelopment.meteorclient.utils.misc.texture.ImageData;
import meteordevelopment.meteorclient.utils.misc.texture.ImageDataFactory;
import meteordevelopment.meteorclient.utils.misc.texture.TextureUtils;
import meteordevelopment.meteorclient.utils.render.color.Color;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
import net.minecraft.util.thread.NameableExecutor;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static meteordevelopment.meteorclient.MeteorClient.*;
import static meteordevelopment.meteorclient.utils.misc.texture.TextureUtils.getCurrentAnimationFrame;
import static meteordevelopment.meteorclient.utils.misc.texture.TextureUtils.registerTexture;
import static org.lwjgl.opengl.GL11C.GL_MAX_TEXTURE_SIZE;
import static org.lwjgl.opengl.GL11C.glGetInteger;

public class ImageHud extends HudElement {
public static final HudElementInfo<ImageHud> INFO = new HudElementInfo<>(Hud.GROUP, "image", "Cures your ADHD.", ImageHud::new);
public static final int MAX_TEX_SIZE = glGetInteger(GL_MAX_TEXTURE_SIZE);
private static final Identifier DEFAULT_TEXTURE = Identifier.of(MOD_ID,"textures/icons/gui/default_image.png");
private static final Identifier LOADING_TEXTURE = Identifier.of(MOD_ID,"textures/icons/gui/loading_image.png");
private static final Color TRANSPARENT = new Color(255, 255, 255, 255);
private static final int DEBOUNCE_TIME = 2; // 2 Seconds before rerunning.
private Identifier texture;
private final NameableExecutor worker = Util.getIoWorkerExecutor();
private ImageData cachedImageData;
private CompletableFuture<Void> currentImageDataFuture;
private CompletableFuture<Void> debounceTask;
private long lastModified = System.currentTimeMillis();
private String lastPath = "";

private final SettingGroup sgGeneral = settings.getDefaultGroup();

public ImageHud() {
super(INFO);
setSize(128,128);
}

private final Setting<String> path = sgGeneral.add(new StringSetting.Builder()
.name("Path")
.description("The full path / link of the image")
.wide()
.onChanged(path -> {
lastModified = System.currentTimeMillis();
if (debounceTask != null && !debounceTask.isDone()) {
debounceTask.cancel(false);
}
debounceTask = CompletableFuture.runAsync(() -> {
if (System.currentTimeMillis() - lastModified > DEBOUNCE_TIME && !lastPath.equals(path)) {
lastPath = path;
composeImage(path);
}
}, CompletableFuture.delayedExecutor(DEBOUNCE_TIME, TimeUnit.SECONDS, mc));
})
.build()
);

public final Setting<Double> scale = sgGeneral.add(new DoubleSetting.Builder()
.name("scale")
.description("Custom scale.")
.defaultValue(1)
.min(0.1)
.sliderRange(0.5, 2)
.max(10)
.onChanged(sc -> {
if (cachedImageData != null) {
setSize(cachedImageData.width * sc, cachedImageData.height * sc);
}
})
.build()
);

/**
* Composes the image asynchronously, since it can be a very slow process for animated images or big ones.
* @param path the URI in String format.
*/
private void composeImage(String path) {
// Parse URI
String parsed = path.replace("\"", "").replace("\\", "/");
String name = parsed.substring(parsed.lastIndexOf("/") + 1);
cachedImageData = null;

currentImageDataFuture = CompletableFuture.supplyAsync(() -> {
try {
InputStream imageFile = path.toLowerCase().startsWith("http") ? new URL(path).openStream() : new FileInputStream(parsed);
ImageInputStream stream = ImageIO.createImageInputStream(imageFile);
ImageReader reader = ImageIO.getImageReaders(stream).next();
reader.setInput(stream);
if (reader.getFormatName().equals("gif")) {
return ImageDataFactory.fromGIF(name, reader);
} else {
return ImageDataFactory.fromStatic(name, reader);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}, worker).exceptionallyAsync( e -> {
LOG.debug("Failed to load image", e);
texture = null;
return null;
}, mc).thenAcceptAsync(data -> {
if (data != null) {
if (texture != null) mc.getTextureManager().destroyTexture(texture);
texture = registerTexture(data);
setSize(data.width * scale.get(), data.height * scale.get());
cachedImageData = data;
}
},mc);
}

@Override
public void render(HudRenderer renderer) {
if (currentImageDataFuture != null && !currentImageDataFuture.isDone()) {
renderer.texture(LOADING_TEXTURE, getX(), getY(), 128, 128, TRANSPARENT, scale.get().floatValue());
}
else if (cachedImageData == null || texture == null) {
renderer.texture(DEFAULT_TEXTURE,getX(),getY(),128,128,TRANSPARENT,scale.get().floatValue());
}
else {
try {
if (cachedImageData.delays.isEmpty()){
renderer.texture(texture, getX(), getY(), cachedImageData.width, cachedImageData.height, TRANSPARENT, scale.get().floatValue());
} else {
renderGif(renderer, cachedImageData, texture, x, y, scale.get().floatValue());
}
} catch (Exception e) {
LOG.debug("Failed to render image", e);
}
}
}

public static void renderGif(HudRenderer renderer, ImageData imageData, Identifier texture, int x, int y, float scale) {
int frameIndex = getCurrentAnimationFrame(imageData.delays);
int row = frameIndex % imageData.framesPerColumn;
int column = frameIndex / imageData.framesPerColumn;
TextureRegion textureRegion = new TextureRegion(imageData.width,imageData.height);

textureRegion.x1 = (float) (column * imageData.width) / imageData.canvasWidth;
textureRegion.y1 = (float) (row * imageData.height) / imageData.canvasHeight;
textureRegion.x2 = (float) ((column + 1) * imageData.width) / imageData.canvasWidth;
textureRegion.y2 = (float) ((row + 1) * imageData.height) / imageData.canvasHeight;

renderer.texture(texture,x,y,imageData.width,imageData.height,textureRegion,TRANSPARENT,scale);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.utils.misc.texture;

import java.awt.*;

public record FrameMetadata(int delay, String disposal, Offset offset, Color backgroundColor) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.utils.misc.texture;

import org.jetbrains.annotations.NotNull;
import org.w3c.dom.NodeList;

import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;

public class FrameMetadataFactory {
/**
* Factory pattern to get the FrameMetadata object to compose a GIF. This factory can be extended in the future in case
* we want other animated formats like APNG or WEBP. (Not that Oracle's jdk supports them yet).
* @param metadata the GIF's IIOMetadata object from the image reader.
* It effectively contains all the metadata from a GIF frame.
* @return FrameMetadata.
*/
public static FrameMetadata fromGIF(IIOMetadata metadata) {
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
IIOMetadataNode desc = (IIOMetadataNode) root.getElementsByTagName("ImageDescriptor").item(0);
int delay = Integer.parseInt(gce.getAttribute("delayTime"));
String disposal = gce.getAttribute("disposalMethod");
int topPos = Integer.parseInt(desc.getAttribute("imageTopPosition"));
int leftPos = Integer.parseInt(desc.getAttribute("imageLeftPosition"));
if (disposal.equals("restoreToBackground")) return new FrameMetadata(delay, disposal, new Offset(leftPos,topPos), getBgColor(root));
return new FrameMetadata(delay, disposal, new Offset(leftPos,topPos), new Color(0,0,0));
}

/**
* Gets the background color from a GIF's metadata. Needs to locate the index of the color in the LogicalScreenDescriptor
* and search it inside the ColorTableEntry list in the GlobalColorTable.
* @param root a IIOMetadataNode object (root) from the IIOMetadata.
* @return Color.
*/
private static java.awt.Color getBgColor(IIOMetadataNode root) {
IIOMetadataNode lsd = (IIOMetadataNode) root.getElementsByTagName("LogicalScreenDescriptor").item(0); // LOL
IIOMetadataNode gct = (IIOMetadataNode) root.getElementsByTagName("GlobalColorTable").item(0);
int bgcIndex = Integer.parseInt(lsd.getAttribute("backgroundColorIndex"));
return getBgColor(gct, bgcIndex);
}

private static @NotNull java.awt.Color getBgColor(IIOMetadataNode gct, int bgcIndex) {
java.awt.Color bgColor = new java.awt.Color(0,0,0);
if (gct != null) {
NodeList colorEntries = gct.getElementsByTagName("ColorTableEntry");
for (int i = 0; i < colorEntries.getLength(); i++) {
IIOMetadataNode colorEntry = (IIOMetadataNode) colorEntries.item(i);
int colorIndex = Integer.parseInt(colorEntry.getAttribute("index"));
if (colorIndex == bgcIndex) {
bgColor = new java.awt.Color(Integer.parseInt(colorEntry.getAttribute("red")),
Integer.parseInt(colorEntry.getAttribute("green")),
Integer.parseInt(colorEntry.getAttribute("blue")));
}
}
}
return bgColor;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.utils.misc.texture;

import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.texture.NativeImage;

import java.util.List;

public class ImageData {
String name;
NativeImage texture;
public int width;
public int height;
public int canvasWidth;
public int canvasHeight;
public int framesPerColumn;
int totalFrames;
public IntList delays;

public ImageData(String name) {
this.name = name;
}

public int getColumns(){
return (int) Math.ceil(totalFrames / (double) framesPerColumn);
}
}
Loading