Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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,154 @@
/*
* 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 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.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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 Identifier texture;
private final ExecutorService worker = Executors.newSingleThreadExecutor();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be Util.getIoWorkerExecutor() or even virtual thread

private ImageData cachedImageData;
private CompletableFuture<Void> currentImageDataFuture;

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(this::composeImage)
.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) {
// Cancel the future immediately to recreate another texture for the user.
if (currentImageDataFuture != null && !currentImageDataFuture.isDone()) {
currentImageDataFuture.cancel(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should debounce the modification rather than canceling?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you suggest a custom delay, or for the original image to finish loading?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, wait the original value stopped to change for 5 seconds, then we start to load.

}
// Parse URI
String parsed = path.replace("\"", "").replace("\\", "/");
String name = parsed.substring(parsed.lastIndexOf("/") + 1);
cachedImageData = null;

currentImageDataFuture = CompletableFuture.supplyAsync(() -> {
try {
InputStream imageFile = path.startsWith("http") ? new URL(path).openStream() : new FileInputStream(parsed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might use Http?

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) {
LOG.debug("Failed to load image", e);
texture = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it still accesses field texture on worker thread. Might move to exceptionallyAsync(..., mc)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or volatile

return null;
}
}, worker).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,30 @@
/*
* 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 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 List<Integer> delays;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be IntList

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the main difference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance, but it might not be important here.


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

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