add server preview renderer
All checks were successful
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Successful in 38s

This commit is contained in:
Lee
2024-04-20 19:37:58 +01:00
parent ff58b1756a
commit d2ae4b4cc5
17 changed files with 344 additions and 17 deletions

View File

@ -3,6 +3,7 @@ package xyz.mcutils.backend.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@ -93,4 +94,20 @@ public final class ColorUtils {
return builder.toString();
}
/**
* Gets a {@link Color} from a Minecraft color code.
*
* @param colorCode the color code to get the color from
* @return the color
*/
public static Color getMinecraftColor(char colorCode) {
String color = COLOR_MAP.getOrDefault(colorCode, null);
if (color == null) {
System.out.println("Unknown color code: " + colorCode);
return Color.WHITE;
}
return Color.decode(color);
}
}

View File

@ -0,0 +1,23 @@
package xyz.mcutils.backend.common;
import xyz.mcutils.backend.Main;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
public class Fonts {
public static final Font MINECRAFT;
public static final Font MINECRAFT_BOLD;
static {
InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
try {
MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
} catch (FontFormatException | IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -8,21 +8,23 @@ import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
@Log4j2(topic = "Image Utils")
public class ImageUtils {
/**
* Scale the given image to the provided size.
* Scale the given image to the provided scale.
*
* @param image the image to scale
* @param size the size to scale the image to
* @param scale the scale to scale the image to
* @return the scaled image
*/
public static BufferedImage resize(BufferedImage image, double size) {
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB);
public static BufferedImage resize(BufferedImage image, double scale) {
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaled.createGraphics();
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null);
graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
graphics.dispose();
return scaled;
}
@ -56,4 +58,21 @@ public class ImageUtils {
throw new Exception("Failed to convert image to bytes", e);
}
}
/**
* Convert a base64 string to an image.
*
* @param base64 the base64 string to convert
* @return the image
*/
@SneakyThrows
public static BufferedImage base64ToImage(String base64) {
String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
try {
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
} catch (Exception e) {
throw new Exception("Failed to convert base64 to image", e);
}
}
}

View File

@ -0,0 +1,14 @@
package xyz.mcutils.backend.common.renderer;
import java.awt.image.BufferedImage;
public abstract class Renderer<T> {
/**
* Renders the object to the specified size.
*
* @param input The object to render.
* @param size The size to render the object to.
*/
public abstract BufferedImage render(T input, int size);
}

View File

@ -0,0 +1,167 @@
package xyz.mcutils.backend.common.renderer.impl.misc;
import lombok.extern.log4j.Log4j2;
import xyz.mcutils.backend.Main;
import xyz.mcutils.backend.common.ColorUtils;
import xyz.mcutils.backend.common.Fonts;
import xyz.mcutils.backend.common.ImageUtils;
import xyz.mcutils.backend.common.renderer.Renderer;
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
import xyz.mcutils.backend.model.server.MinecraftServer;
import xyz.mcutils.backend.service.ServerService;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@Log4j2
public class ServerPreviewRenderer extends Renderer<MinecraftServer> {
public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
private static BufferedImage SERVER_BACKGROUND;
private static BufferedImage PING_ICON;
static {
try {
SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
} catch (Exception ex) {
log.error("Failed to load server preview assets", ex);
}
}
private final int fontSize = Fonts.MINECRAFT.getSize();
private final int width = 560;
private final int height = 64 + 3 + 3;
private final int padding = 3;
@Override
public BufferedImage render(MinecraftServer server, int size) {
BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
BufferedImage favicon = getServerFavicon(server);
BufferedImage background = SERVER_BACKGROUND;
// Create the graphics for drawing
Graphics2D graphics = texture.createGraphics();
// Set up the font
graphics.setFont(Fonts.MINECRAFT);
// Draw the background
for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
graphics.drawImage(background, backgroundX, backgroundY, null);
}
}
int y = fontSize + 1;
int x = 64 + 8;
int initialX = x; // Store the initial value of x
// Draw the favicon
graphics.drawImage(favicon, padding, padding, null);
// Draw the server hostname
graphics.setColor(Color.WHITE);
graphics.drawString(server.getHostname(), x, y);
// Draw the server motd
y += fontSize + (padding * 2);
for (String line : server.getMotd().getRaw()) {
int index = 0;
int colorIndex = line.indexOf("§");
while (colorIndex != -1) {
// Draw text before color code
String textBeforeColor = line.substring(index, colorIndex);
graphics.drawString(textBeforeColor, x, y);
// Calculate width of text before color code
int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
// Move x position to after the drawn text
x += textWidth;
// Set color based on color code
char colorCode = line.charAt(colorIndex + 1);
if (colorCode == 'l') {
graphics.setFont(Fonts.MINECRAFT_BOLD);
} else {
Color color = ColorUtils.getMinecraftColor(colorCode);
graphics.setColor(color);
graphics.setFont(Fonts.MINECRAFT);
}
// Move index to after the color code
index = colorIndex + 2;
// Find next color code
colorIndex = line.indexOf("§", index);
}
// Draw remaining text
String remainingText = line.substring(index);
graphics.drawString(remainingText, x, y);
// Move to the next line
y += fontSize + padding;
// Reset x position for the next line
x = initialX; // Reset x to its initial value
}
// Ensure the font is reset
graphics.setFont(Fonts.MINECRAFT);
// Render the ping
BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
x = width - pingIcon.getWidth() - padding;
graphics.drawImage(pingIcon, x, padding, null);
// Reset the y position
y = fontSize + 1;
// Render the player count
MinecraftServer.Players players = server.getPlayers();
String playersOnline = players.getOnline() + "";
String playersMax = players.getMax() + "";
// Calculate the width of each player count element
int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
int slashWidth = graphics.getFontMetrics().stringWidth("/");
int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
// Calculate the total width of the player count string
int totalWidth = maxWidth + slashWidth + onlineWidth;
// Calculate the starting x position
int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
// Render the player count elements
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersOnline, startX, y);
startX += onlineWidth;
graphics.setColor(Color.DARK_GRAY);
graphics.drawString("/", startX, y);
startX += slashWidth;
graphics.setColor(Color.LIGHT_GRAY);
graphics.drawString(playersMax, startX, y);
return ImageUtils.resize(texture, (double) size / width);
}
/**
* Get the favicon of a server.
*
* @param server the server to get the favicon of
* @return the server favicon
*/
public BufferedImage getServerFavicon(MinecraftServer server) {
String favicon = null;
// Get the server favicon
if (server instanceof JavaMinecraftServer javaServer) {
if (javaServer.getFavicon() != null) {
favicon = javaServer.getFavicon().getBase64();
}
}
// Fallback to the default server icon
if (favicon == null) {
favicon = ServerService.DEFAULT_SERVER_ICON;
}
return ImageUtils.base64ToImage(favicon);
}
}

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
import xyz.mcutils.backend.model.skin.ISkinPart;

View File

@ -1,4 +1,4 @@
package xyz.mcutils.backend.common.renderer.impl;
package xyz.mcutils.backend.common.renderer.impl.skin;
import lombok.AllArgsConstructor;
import lombok.Getter;