forked from MinecraftUtilities/Backend
add server preview renderer
This commit is contained in:
parent
ff58b1756a
commit
d2ae4b4cc5
@ -3,6 +3,7 @@ package xyz.mcutils.backend.common;
|
|||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -93,4 +94,20 @@ public final class ColorUtils {
|
|||||||
|
|
||||||
return builder.toString();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
23
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal file
23
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,21 +8,23 @@ import javax.imageio.ImageIO;
|
|||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
@Log4j2(topic = "Image Utils")
|
@Log4j2(topic = "Image Utils")
|
||||||
public class ImageUtils {
|
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 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
|
* @return the scaled image
|
||||||
*/
|
*/
|
||||||
public static BufferedImage resize(BufferedImage image, double size) {
|
public static BufferedImage resize(BufferedImage image, double scale) {
|
||||||
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * size), (int) (image.getHeight() * size), BufferedImage.TYPE_INT_ARGB);
|
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
|
||||||
Graphics2D graphics = scaled.createGraphics();
|
Graphics2D graphics = scaled.createGraphics();
|
||||||
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null);
|
graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
|
||||||
graphics.dispose();
|
graphics.dispose();
|
||||||
return scaled;
|
return scaled;
|
||||||
}
|
}
|
||||||
@ -56,4 +58,21 @@ public class ImageUtils {
|
|||||||
throw new Exception("Failed to convert image to bytes", e);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
167
src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java
Normal file
167
src/main/java/xyz/mcutils/backend/common/renderer/impl/misc/ServerPreviewRenderer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package xyz.mcutils.backend.common.renderer.impl;
|
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
@ -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.common.renderer.IsometricSkinRenderer;
|
||||||
import xyz.mcutils.backend.model.skin.ISkinPart;
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
@ -1,4 +1,4 @@
|
|||||||
package xyz.mcutils.backend.common.renderer.impl;
|
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
@ -56,6 +56,23 @@ public class ServerController {
|
|||||||
.body(favicon);
|
.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
|
@ResponseBody
|
||||||
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public ResponseEntity<?> getServerBlockedStatus(
|
public ResponseEntity<?> getServerBlockedStatus(
|
||||||
|
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
@ -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;
|
||||||
|
}
|
@ -3,9 +3,9 @@ package xyz.mcutils.backend.model.skin;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
||||||
import xyz.mcutils.backend.common.renderer.impl.BodyRenderer;
|
import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer;
|
||||||
import xyz.mcutils.backend.common.renderer.impl.IsometricHeadRenderer;
|
import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer;
|
||||||
import xyz.mcutils.backend.common.renderer.impl.SquareRenderer;
|
import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
@ -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<CachedServerPreview, String> { }
|
@ -135,12 +135,10 @@ public class PlayerService {
|
|||||||
*/
|
*/
|
||||||
public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) {
|
public CachedPlayerSkinPart getSkinPart(Player player, String partName, boolean renderOverlay, int size) {
|
||||||
if (size > 512) {
|
if (size > 512) {
|
||||||
log.info("Size {} is too large, setting to 512", size);
|
throw new BadRequestException("Size cannot be larger than 512");
|
||||||
size = 512;
|
|
||||||
}
|
}
|
||||||
if (size < 32) {
|
if (size < 32) {
|
||||||
log.info("Size {} is too small, setting to 32", size);
|
throw new BadRequestException("Size cannot be smaller than 32");
|
||||||
size = 32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ISkinPart part = ISkinPart.getByName(partName); // The skin part to get
|
ISkinPart part = ISkinPart.getByName(partName); // The skin part to get
|
||||||
|
@ -6,15 +6,19 @@ import org.springframework.stereotype.Service;
|
|||||||
import xyz.mcutils.backend.common.AppConfig;
|
import xyz.mcutils.backend.common.AppConfig;
|
||||||
import xyz.mcutils.backend.common.DNSUtils;
|
import xyz.mcutils.backend.common.DNSUtils;
|
||||||
import xyz.mcutils.backend.common.EnumUtils;
|
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.BadRequestException;
|
||||||
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
|
import xyz.mcutils.backend.exception.impl.ResourceNotFoundException;
|
||||||
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
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.DNSRecord;
|
||||||
import xyz.mcutils.backend.model.dns.impl.ARecord;
|
import xyz.mcutils.backend.model.dns.impl.ARecord;
|
||||||
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
|
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
|
||||||
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||||
import xyz.mcutils.backend.model.server.MinecraftServer;
|
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||||
import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository;
|
import xyz.mcutils.backend.repository.redis.MinecraftServerCacheRepository;
|
||||||
|
import xyz.mcutils.backend.repository.redis.ServerPreviewCacheRepository;
|
||||||
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
|
import xyz.mcutils.backend.service.metric.metrics.UniqueServerLookupsMetric;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@ -25,17 +29,20 @@ import java.util.Optional;
|
|||||||
|
|
||||||
@Service @Log4j2(topic = "Server Service")
|
@Service @Log4j2(topic = "Server Service")
|
||||||
public class ServerService {
|
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 MojangService mojangService;
|
||||||
private final MetricService metricService;
|
private final MetricService metricService;
|
||||||
private final MinecraftServerCacheRepository serverCacheRepository;
|
private final MinecraftServerCacheRepository serverCacheRepository;
|
||||||
|
private final ServerPreviewCacheRepository serverPreviewCacheRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository) {
|
public ServerService(MojangService mojangService, MetricService metricService, MinecraftServerCacheRepository serverCacheRepository,
|
||||||
|
ServerPreviewCacheRepository serverPreviewCacheRepository) {
|
||||||
this.mojangService = mojangService;
|
this.mojangService = mojangService;
|
||||||
this.metricService = metricService;
|
this.metricService = metricService;
|
||||||
this.serverCacheRepository = serverCacheRepository;
|
this.serverCacheRepository = serverCacheRepository;
|
||||||
|
this.serverPreviewCacheRepository = serverPreviewCacheRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,4 +138,39 @@ public class ServerService {
|
|||||||
}
|
}
|
||||||
return Base64.getDecoder().decode(icon); // Return the decoded favicon
|
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<CachedServerPreview> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
BIN
src/main/resources/fonts/minecraft-font.ttf
Normal file
BIN
src/main/resources/fonts/minecraft-font.ttf
Normal file
Binary file not shown.
BIN
src/main/resources/icons/ping.png
Normal file
BIN
src/main/resources/icons/ping.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 B |
BIN
src/main/resources/icons/server_background.png
Normal file
BIN
src/main/resources/icons/server_background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 593 B |
Loading…
Reference in New Issue
Block a user