From d2ae4b4cc52fbdd0807d6cf2588686ee701856b8 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 20 Apr 2024 19:37:58 +0100 Subject: [PATCH] add server preview renderer --- .../mcutils/backend/common/ColorUtils.java | 17 ++ .../xyz/mcutils/backend/common/Fonts.java | 23 +++ .../mcutils/backend/common/ImageUtils.java | 29 ++- .../backend/common/renderer/Renderer.java | 14 ++ .../impl/misc/ServerPreviewRenderer.java | 167 ++++++++++++++++++ .../impl/{ => skin}/BodyRenderer.java | 2 +- .../{ => skin}/IsometricHeadRenderer.java | 2 +- .../impl/{ => skin}/SquareRenderer.java | 2 +- .../backend/controller/ServerController.java | 17 ++ .../model/cache/CachedServerPreview.java | 21 +++ .../mcutils/backend/model/skin/ISkinPart.java | 6 +- .../redis/ServerPreviewCacheRepository.java | 9 + .../backend/service/PlayerService.java | 6 +- .../backend/service/ServerService.java | 46 ++++- src/main/resources/fonts/minecraft-font.ttf | Bin 0 -> 160568 bytes src/main/resources/icons/ping.png | Bin 0 -> 105 bytes .../resources/icons/server_background.png | Bin 0 -> 593 bytes 17 files changed, 344 insertions(+), 17 deletions(-) create mode 100644 src/main/java/xyz/mcutils/backend/common/Fonts.java create mode 100644 src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java create mode 100644 src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java rename src/main/java/xyz/mcutils/backend/common/renderer/impl/{ => skin}/BodyRenderer.java (97%) rename src/main/java/xyz/mcutils/backend/common/renderer/impl/{ => skin}/IsometricHeadRenderer.java (97%) rename src/main/java/xyz/mcutils/backend/common/renderer/impl/{ => skin}/SquareRenderer.java (95%) create mode 100644 src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java create mode 100644 src/main/java/xyz/mcutils/backend/repository/redis/ServerPreviewCacheRepository.java create mode 100644 src/main/resources/fonts/minecraft-font.ttf create mode 100644 src/main/resources/icons/ping.png create mode 100644 src/main/resources/icons/server_background.png diff --git a/src/main/java/xyz/mcutils/backend/common/ColorUtils.java b/src/main/java/xyz/mcutils/backend/common/ColorUtils.java index 8e628c6..38a1715 100644 --- a/src/main/java/xyz/mcutils/backend/common/ColorUtils.java +++ b/src/main/java/xyz/mcutils/backend/common/ColorUtils.java @@ -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); + } + } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/common/Fonts.java b/src/main/java/xyz/mcutils/backend/common/Fonts.java new file mode 100644 index 0000000..216aab2 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/common/Fonts.java @@ -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); + } + } +} diff --git a/src/main/java/xyz/mcutils/backend/common/ImageUtils.java b/src/main/java/xyz/mcutils/backend/common/ImageUtils.java index fd9dc90..5aa86d4 100644 --- a/src/main/java/xyz/mcutils/backend/common/ImageUtils.java +++ b/src/main/java/xyz/mcutils/backend/common/ImageUtils.java @@ -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); + } + } } diff --git a/src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java b/src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java new file mode 100644 index 0000000..c64324d --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java @@ -0,0 +1,14 @@ +package xyz.mcutils.backend.common.renderer; + +import java.awt.image.BufferedImage; + +public abstract class Renderer { + + /** + * 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); +} diff --git a/src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java b/src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java new file mode 100644 index 0000000..38ee70e --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java @@ -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 { + 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); + } +} diff --git a/src/main/java/xyz/mcutils/backend/common/renderer/impl/BodyRenderer.java b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/BodyRenderer.java similarity index 97% rename from src/main/java/xyz/mcutils/backend/common/renderer/impl/BodyRenderer.java rename to src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/BodyRenderer.java index 50f6d35..1aab626 100644 --- a/src/main/java/xyz/mcutils/backend/common/renderer/impl/BodyRenderer.java +++ b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/BodyRenderer.java @@ -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; diff --git a/src/main/java/xyz/mcutils/backend/common/renderer/impl/IsometricHeadRenderer.java b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java similarity index 97% rename from src/main/java/xyz/mcutils/backend/common/renderer/impl/IsometricHeadRenderer.java rename to src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java index f87c1e2..66074bb 100644 --- a/src/main/java/xyz/mcutils/backend/common/renderer/impl/IsometricHeadRenderer.java +++ b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java @@ -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; diff --git a/src/main/java/xyz/mcutils/backend/common/renderer/impl/SquareRenderer.java b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/SquareRenderer.java similarity index 95% rename from src/main/java/xyz/mcutils/backend/common/renderer/impl/SquareRenderer.java rename to src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/SquareRenderer.java index 214111e..9049b18 100644 --- a/src/main/java/xyz/mcutils/backend/common/renderer/impl/SquareRenderer.java +++ b/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/SquareRenderer.java @@ -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; diff --git a/src/main/java/xyz/mcutils/backend/controller/ServerController.java b/src/main/java/xyz/mcutils/backend/controller/ServerController.java index 9fb1127..244df7d 100644 --- a/src/main/java/xyz/mcutils/backend/controller/ServerController.java +++ b/src/main/java/xyz/mcutils/backend/controller/ServerController.java @@ -56,6 +56,23 @@ public class ServerController { .body(favicon); } + @ResponseBody + @GetMapping(value = "/preview/{platform}/{hostname}", produces = MediaType.IMAGE_PNG_VALUE) + public ResponseEntity getServerPreview( + @Parameter(description = "The platform of the server", example = "java") @PathVariable String platform, + @Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname, + @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download, + @Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) { + String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png"; + CachedMinecraftServer server = serverService.getServer(platform, hostname); + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()) + .contentType(MediaType.IMAGE_PNG) + .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname)) + .body(serverService.getServerPreview(server, platform, size)); + } + @ResponseBody @GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getServerBlockedStatus( diff --git a/src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java b/src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java new file mode 100644 index 0000000..2c1134e --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java @@ -0,0 +1,21 @@ +package xyz.mcutils.backend.model.cache; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@AllArgsConstructor +@Setter @Getter @EqualsAndHashCode +@RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds) +public class CachedServerPreview { + + /** + * The ID of the server preview + */ + @Id @NonNull private String id; + + /** + * The server preview bytes + */ + private byte[] bytes; +} diff --git a/src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java b/src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java index 17ebca5..2286123 100644 --- a/src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java +++ b/src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java @@ -3,9 +3,9 @@ package xyz.mcutils.backend.model.skin; import lombok.AllArgsConstructor; import lombok.Getter; import xyz.mcutils.backend.common.renderer.SkinRenderer; -import xyz.mcutils.backend.common.renderer.impl.BodyRenderer; -import xyz.mcutils.backend.common.renderer.impl.IsometricHeadRenderer; -import xyz.mcutils.backend.common.renderer.impl.SquareRenderer; +import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer; +import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer; +import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer; import java.awt.image.BufferedImage; diff --git a/src/main/java/xyz/mcutils/backend/repository/redis/ServerPreviewCacheRepository.java b/src/main/java/xyz/mcutils/backend/repository/redis/ServerPreviewCacheRepository.java new file mode 100644 index 0000000..13ad103 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/repository/redis/ServerPreviewCacheRepository.java @@ -0,0 +1,9 @@ +package xyz.mcutils.backend.repository.redis; + +import org.springframework.data.repository.CrudRepository; +import xyz.mcutils.backend.model.cache.CachedServerPreview; + +/** + * A cache repository for server previews. + */ +public interface ServerPreviewCacheRepository extends CrudRepository { } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/service/PlayerService.java b/src/main/java/xyz/mcutils/backend/service/PlayerService.java index 649cf38..94d27ce 100644 --- a/src/main/java/xyz/mcutils/backend/service/PlayerService.java +++ b/src/main/java/xyz/mcutils/backend/service/PlayerService.java @@ -135,12 +135,10 @@ public class PlayerService { */ public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) { if (size > 512) { - log.info("Size {} is too large, setting to 512", size); - size = 512; + throw new BadRequestException("Size cannot be larger than 512"); } if (size < 32) { - log.info("Size {} is too small, setting to 32", size); - size = 32; + throw new BadRequestException("Size cannot be smaller than 32"); } ISkinPart part = ISkinPart.getByName(partName); // The skin part to get diff --git a/src/main/java/xyz/mcutils/backend/service/ServerService.java b/src/main/java/xyz/mcutils/backend/service/ServerService.java index a18b861..05e3802 100644 --- a/src/main/java/xyz/mcutils/backend/service/ServerService.java +++ b/src/main/java/xyz/mcutils/backend/service/ServerService.java @@ -6,15 +6,19 @@ import org.springframework.stereotype.Service; import xyz.mcutils.backend.common.AppConfig; import xyz.mcutils.backend.common.DNSUtils; import xyz.mcutils.backend.common.EnumUtils; +import xyz.mcutils.backend.common.ImageUtils; +import xyz.mcutils.backend.common.renderer.impl.misc.ServerPreviewRenderer; import xyz.mcutils.backend.exception.impl.BadRequestException; import xyz.mcutils.backend.exception.impl.ResourceNotFoundException; import xyz.mcutils.backend.model.cache.CachedMinecraftServer; +import xyz.mcutils.backend.model.cache.CachedServerPreview; import xyz.mcutils.backend.model.dns.DNSRecord; import xyz.mcutils.backend.model.dns.impl.ARecord; import xyz.mcutils.backend.model.dns.impl.SRVRecord; import xyz.mcutils.backend.model.server.JavaMinecraftServer; import xyz.mcutils.backend.model.server.MinecraftServer; import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository; +import xyz.mcutils.backend.repository.redis.ServerPreviewCacheRepository; import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric; import java.net.InetSocketAddress; @@ -25,17 +29,20 @@ import java.util.Optional; @Service @Log4j2(topic = "Server Service") public class ServerService { - private static final String DEFAULT_SERVER_ICON = "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"; + public static final String DEFAULT_SERVER_ICON = "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"; private final MojangService mojangService; private final MetricService metricService; private final MinecraftServerCacheRepository serverCacheRepository; + private final ServerPreviewCacheRepository serverPreviewCacheRepository; @Autowired - public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository) { + public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository, + ServerPreviewCacheRepository serverPreviewCacheRepository) { this.mojangService = mojangService; this.metricService = metricService; this.serverCacheRepository = serverCacheRepository; + this.serverPreviewCacheRepository = serverPreviewCacheRepository; } /** @@ -131,4 +138,39 @@ public class ServerService { } return Base64.getDecoder().decode(icon); // Return the decoded favicon } + + /** + * Gets the server list preview image. + * + * @param cachedServer the server to get the preview of + * @param platform the platform of the server + * @param size the size of the preview + * @return the server preview + */ + public byte[] getServerPreview(CachedMinecraftServer cachedServer, String platform, int size) { + if (size > 2048) { + throw new BadRequestException("Size cannot be greater than 2048"); + } + if (size < 256) { + throw new BadRequestException("Size cannot be smaller than 256"); + } + MinecraftServer server = cachedServer.getServer(); + log.info("Getting preview for server: {}:{} (size {})", server.getHostname(), server.getPort(), size); + String key = "%s-%s:%s".formatted(platform, server.getHostname(), server.getPort()); + + // Check if the server preview is cached + Optional cached = serverPreviewCacheRepository.findById(key); + if (cached.isPresent() && AppConfig.isProduction()) { + log.info("Server preview for {}:{} is cached", server.getHostname(), server.getPort()); + return cached.get().getBytes(); + } + + long start = System.currentTimeMillis(); + byte[] preview = ImageUtils.imageToBytes(ServerPreviewRenderer.INSTANCE.render(server, size)); + log.info("Took {}ms to render preview for server: {}:{}", System.currentTimeMillis() - start, server.getHostname(), server.getPort()); + + CachedServerPreview serverPreview = new CachedServerPreview(key, preview); + serverPreviewCacheRepository.save(serverPreview); + return preview; + } } diff --git a/src/main/resources/fonts/minecraft-font.ttf b/src/main/resources/fonts/minecraft-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d1cd52568290123b7ff8870faf1760ed9d298a65 GIT binary patch literal 160568 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 Ah5!Hn literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/ping.png b/src/main/resources/icons/ping.png new file mode 100644 index 0000000000000000000000000000000000000000..f152a0a40e4975cef993ff7631be05f08bbdcc3d GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^AT|dF8<0HkD{mW+GVpY945^4qPFNuIWBm{LSPszx zA`LIf>U6p;iY90rS<0|(!UQIf)*C7pG7TCGKU`q2UEn2H6&|+$sF%Uh)z4*}Q$iB} D&+{Oz literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/server_background.png b/src/main/resources/icons/server_background.png new file mode 100644 index 0000000000000000000000000000000000000000..91b485d7eb87b77569fda66083bf3f9daf3f5e2c GIT binary patch literal 593 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|eg=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C)>m@Ck9{=Vs^TU=`-$6c^+X6cps+-tI2!Vu>BEfgH{PkH}&M2EM}}%y>M1MG8=my~NYkmHh>ah=3MDeyNo_ zNK<~1UkKy*Mdh=AoNJygjv*e$_fEaYd)R=(CGug|39dBTcMBdK=I~&C6$?`?-`zz@l0#jE9AX| zvEVm{m-3bgd<&lJeExYyuwLrD?`NzSn9Fvriv8hv^&IoUNm8xTRd!xvyrQVOj;&Bu zZw)httxzh{7J=C45(d?^Zocro|WBTHtoIYdTRL@+7DLo5&0{Vg?|70@M%(Hbc z5=eT%ka8>a`K0L?M-E@_W7;$CIYWK_`N{{!&;MmLTEBYc+=@%qKp(1>xJHzuB$lLF zB^RXvDF!10BNJT%6I~;N5JNL76GJN#18oBXD+7b>OAb~j8glbfGSez?YxvdwqYbD* z18ze}W^QV6Nn&mRx*j8-@eoT6JL;{ literal 0 HcmV?d00001