add more to the server response
All checks were successful
ci / deploy (push) Successful in 1m14s

This commit is contained in:
Lee 2024-04-10 09:19:02 +01:00
parent c7fe26ef8f
commit 330c3efc78
12 changed files with 540 additions and 19 deletions

@ -0,0 +1,81 @@
package cc.fascinated.common;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* @author Braydon
*/
@UtilityClass
public final class ColorUtils {
private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)§[0-9A-FK-OR]");
private static final Map<Character, String> COLOR_MAP = new HashMap<>();
static {
// Map each color to its corresponding hex code
COLOR_MAP.put('0', "#000000"); // Black
COLOR_MAP.put('1', "#0000AA"); // Dark Blue
COLOR_MAP.put('2', "#00AA00"); // Dark Green
COLOR_MAP.put('3', "#00AAAA"); // Dark Aqua
COLOR_MAP.put('4', "#AA0000"); // Dark Red
COLOR_MAP.put('5', "#AA00AA"); // Dark Purple
COLOR_MAP.put('6', "#FFAA00"); // Gold
COLOR_MAP.put('7', "#AAAAAA"); // Gray
COLOR_MAP.put('8', "#555555"); // Dark Gray
COLOR_MAP.put('9', "#5555FF"); // Blue
COLOR_MAP.put('a', "#55FF55"); // Green
COLOR_MAP.put('b', "#55FFFF"); // Aqua
COLOR_MAP.put('c', "#FF5555"); // Red
COLOR_MAP.put('d', "#FF55FF"); // Light Purple
COLOR_MAP.put('e', "#FFFF55"); // Yellow
COLOR_MAP.put('f', "#FFFFFF"); // White
}
/**
* Strip the color codes
* from the given input.
*
* @param input the input to strip
* @return the stripped input
*/
@NonNull
public static String stripColor(@NonNull String input) {
return STRIP_COLOR_PATTERN.matcher(input).replaceAll("");
}
/**
* Convert the given input into HTML format.
* <p>
* This will replace each color code with
* a span tag with the respective color in
* hex format.
* </p>
*
* @param input the input to convert
* @return the converted input
*/
@NonNull
public static String toHTML(@NonNull String input) {
StringBuilder builder = new StringBuilder();
boolean nextIsColor = false; // Is the next char a color code?
for (char character : input.toCharArray()) {
// Found color symbol, next color is the color
if (character == '§') {
nextIsColor = true;
continue;
}
if (nextIsColor) { // Map the current color to its hex code
String color = COLOR_MAP.getOrDefault(Character.toLowerCase(character), "");
builder.append("<span style=\"color:").append(color).append("\">");
nextIsColor = false;
continue;
}
builder.append(character); // Append the char...
}
return builder.toString();
}
}

@ -1,4 +1,4 @@
package cc.fascinated; package cc.fascinated.common;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;

@ -0,0 +1,187 @@
package cc.fascinated.common;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
/**
* @author Braydon
* @see <a href="https://wiki.vg/Protocol_version_numbers">Protocol Version Numbers</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-16">Spigot NMS (1.16+)</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-10-1-15">Spigot NMS (1.10 - 1.15)</a>
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-legacy">Spigot NMS (1.8 - 1.9)</a>
*/
@RequiredArgsConstructor @Getter @ToString @Log4j2(topic = "Minecraft Version")
public enum JavaMinecraftVersion {
V1_20_3(765, "v1_20_R3"), // 1.20.3 & 1.20.4
V1_20_2(764, "v1_20_R2"), // 1.20.2
V1_20(763, "v1_20_R1"), // 1.20 & 1.20.1
V1_19_4(762, "v1_19_R3"), // 1.19.4
V1_19_3(761, "v1_19_R2"), // 1.19.3
V1_19_1(760, "v1_19_R1"), // 1.19.1 & 1.19.2
V1_19(759, "v1_19_R1"), // 1.19
V1_18_2(758, "v1_18_R2"), // 1.18.2
V1_18(757, "v1_18_R1"), // 1.18 & 1.18.1
V1_17_1(756, "v1_17_R1"), // 1.17.1
V1_17(755, "v1_17_R1"), // 1.17
V1_16_4(754, "v1_16_R3"), // 1.16.4 & 1.16.5
V1_16_3(753, "v1_16_R2"), // 1.16.3
V1_16_2(751, "v1_16_R2"), // 1.16.2
V1_16_1(736, "v1_16_R1"), // 1.16.1
V1_16(735, "v1_16_R1"), // 1.16
V1_15_2(578, "v1_15_R1"), // 1.15.2
V1_15_1(575, "v1_15_R1"), // 1.15.1
V1_15(573, "v1_15_R1"), // 1.15
V1_14_4(498, "v1_14_R1"), // 1.14.4
V1_14_3(490, "v1_14_R1"), // 1.14.3
V1_14_2(485, "v1_14_R1"), // 1.14.2
V1_14_1(480, "v1_14_R1"), // 1.14.1
V1_14(477, "v1_14_R1"), // 1.14
V1_13_2(404, "v1_13_R2"), // 1.13.2
V1_13_1(401, "v1_13_R2"), // 1.13.1
V1_13(393, "v1_13_R1"), // 1.13
V1_12_2(340, "v1_12_R1"), // 1.12.2
V1_12_1(338, "v1_12_R1"), // 1.12.1
V1_12(335, "v1_12_R1"), // 1.12
V1_11_1(316, "v1_11_R1"), // 1.11.1 & 1.11.2
V1_11(315, "v1_11_R1"), // 1.11
V1_10(210, "v1_10_R1"), // 1.10.x
V1_9_3(110, "v1_9_R2"), // 1.9.3 & 1.9.4
V1_9_2(109, "v1_9_R1"), // 1.9.2
V1_9_1(108, "v1_9_R1"), // 1.9.1
V1_9(107, "v1_9_R1"), // 1.9
V1_8(47, "v1_8_R3"), // 1.8.x
UNKNOWN(-1, "Unknown");
// Game Updates
public static final JavaMinecraftVersion TRAILS_AND_TALES = JavaMinecraftVersion.V1_20;
public static final JavaMinecraftVersion THE_WILD_UPDATE = JavaMinecraftVersion.V1_19;
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_2 = JavaMinecraftVersion.V1_18;
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_1 = JavaMinecraftVersion.V1_17;
public static final JavaMinecraftVersion NETHER_UPDATE = JavaMinecraftVersion.V1_16;
public static final JavaMinecraftVersion BUZZY_BEES = JavaMinecraftVersion.V1_15;
public static final JavaMinecraftVersion VILLAGE_AND_PILLAGE = JavaMinecraftVersion.V1_14;
public static final JavaMinecraftVersion UPDATE_AQUATIC = JavaMinecraftVersion.V1_13;
public static final JavaMinecraftVersion WORLD_OF_COLOR_UPDATE = JavaMinecraftVersion.V1_12;
public static final JavaMinecraftVersion EXPLORATION_UPDATE = JavaMinecraftVersion.V1_11;
public static final JavaMinecraftVersion FROSTBURN_UPDATE = JavaMinecraftVersion.V1_10;
public static final JavaMinecraftVersion THE_COMBAT_UPDATE = JavaMinecraftVersion.V1_9;
public static final JavaMinecraftVersion BOUNTIFUL_UPDATE = JavaMinecraftVersion.V1_8;
/**
* The protocol number of this version.
*/
private final int protocol;
/**
* The server version for this version.
*/
private final String nmsVersion;
/**
* The cached name of this version.
*/
private String name;
/**
* Get the name of this protocol version.
*
* @return the name
*/
public String getName() {
// We have a name
if (this.name != null) {
return this.name;
}
// Use the server version as the name if unknown
if (this == UNKNOWN) {
this.name = this.getNmsVersion();
} else { // Parse the name
this.name = name().substring(1);
this.name = this.name.replace("_", ".");
}
return this.name;
}
/**
* Is this version legacy?
*
* @return whether this version is legacy
*/
public boolean isLegacy() {
return this.isBelow(JavaMinecraftVersion.V1_16);
}
/**
* Check if this version is
* above the one given.
*
* @param other the other version
* @return true if above, otherwise false
*/
public boolean isAbove(JavaMinecraftVersion other) {
return this.protocol > other.getProtocol();
}
/**
* Check if this version is
* or above the one given.
*
* @param other the other version
* @return true if is or above, otherwise false
*/
public boolean isOrAbove(JavaMinecraftVersion other) {
return this.protocol >= other.getProtocol();
}
/**
* Check if this version is
* below the one given.
*
* @param other the other version
* @return true if below, otherwise false
*/
public boolean isBelow(JavaMinecraftVersion other) {
return this.protocol < other.getProtocol();
}
/**
* Check if this version is
* or below the one given.
*
* @param other the other version
* @return true if is or below, otherwise false
*/
public boolean isOrBelow(JavaMinecraftVersion other) {
return this.protocol <= other.getProtocol();
}
/**
* Get the version from the given protocol.
*
* @param protocol the protocol to get the version for
* @return the version, null if none
*/
public static JavaMinecraftVersion byProtocol(int protocol) {
for (JavaMinecraftVersion version : values()) {
if (version.getProtocol() == protocol) {
return version;
}
}
return null;
}
}

@ -0,0 +1,30 @@
package cc.fascinated.common;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ServerUtils {
/**
* Get the hostname and port from a hostname string
*
* @param hostname the hostname string
* @return the hostname and port
*/
public static Tuple<String, Integer> getHostnameAndPort(String hostname) {
String[] split = hostname.split(":");
if (split.length == 1) {
return new Tuple<>(split[0], 25565);
}
return new Tuple<>(split[0], Integer.parseInt(split[1]));
}
/**
* Gets the address of the server.
*
* @return the address of the server
*/
public static String getAddress(String ip, int port) {
return ip + (port == 25565 ? "" : ":" + port);
}
}

@ -6,6 +6,7 @@ import cc.fascinated.model.player.Skin;
import cc.fascinated.service.PlayerService; import cc.fascinated.service.PlayerService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -43,6 +44,7 @@ public class PlayerController {
return ResponseEntity.ok() return ResponseEntity.ok()
.cacheControl(cacheControl) .cacheControl(cacheControl)
.contentType(MediaType.IMAGE_PNG) .contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=%s.png".formatted(player.getUsername()))
.body(PlayerUtils.getSkinPartBytes(player.getSkin(), skinPart, size)); .body(PlayerUtils.getSkinPartBytes(player.getSkin(), skinPart, size));
} }
} }

@ -1,10 +1,14 @@
package cc.fascinated.controller; package cc.fascinated.controller;
import cc.fascinated.common.ServerUtils;
import cc.fascinated.common.Tuple;
import cc.fascinated.model.cache.CachedMinecraftServer; import cc.fascinated.model.cache.CachedMinecraftServer;
import cc.fascinated.model.server.JavaMinecraftServer;
import cc.fascinated.model.server.MinecraftServer; import cc.fascinated.model.server.MinecraftServer;
import cc.fascinated.service.ServerService; import cc.fascinated.service.ServerService;
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger; import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -19,14 +23,19 @@ public class ServerController {
@ResponseBody @ResponseBody
@GetMapping(value = "/{platform}/{hostnameAndPort}", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/{platform}/{hostnameAndPort}", produces = MediaType.APPLICATION_JSON_VALUE)
public CachedMinecraftServer getServer(@PathVariable String platform, @PathVariable String hostnameAndPort) { public CachedMinecraftServer getServer(@PathVariable String platform, @PathVariable String hostnameAndPort) {
String[] split = hostnameAndPort.split(":"); Tuple<String, Integer> host = ServerUtils.getHostnameAndPort(hostnameAndPort);
String hostname = split[0]; return serverService.getServer(platform, host.getLeft(), host.getRight());
int port = 25565; }
if (split.length == 2) {
try { @ResponseBody
port = Integer.parseInt(split[1]); @GetMapping(value = "/icon/{hostnameAndPort}", produces = MediaType.APPLICATION_JSON_VALUE)
} catch (NumberFormatException ignored) {} public ResponseEntity<?> getServerIcon(@PathVariable String hostnameAndPort) {
} Tuple<String, Integer> host = ServerUtils.getHostnameAndPort(hostnameAndPort);
return serverService.getServer(platform, hostname, port); String hostname = host.getLeft();
int port = host.getRight();
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=%s.png".formatted(ServerUtils.getAddress(hostname, port)))
.body(serverService.getServerFavicon(hostname, port));
} }
} }

@ -1,5 +1,6 @@
package cc.fascinated.model.mojang; package cc.fascinated.model.mojang;
import cc.fascinated.model.server.JavaMinecraftServer;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
@ -9,5 +10,24 @@ import lombok.ToString;
*/ */
@AllArgsConstructor @Getter @ToString @AllArgsConstructor @Getter @ToString
public final class JavaServerStatusToken { public final class JavaServerStatusToken {
/**
* The version of the server.
*/
private final JavaMinecraftServer.Version version;
/**
* The players on the server.
*/
private final JavaMinecraftServer.Players players;
/**
* The motd of the server.
*/
private final String description; private final String description;
}
/**
* The favicon of the server.
*/
private final String favicon;
}

@ -1,10 +1,128 @@
package cc.fascinated.model.server; package cc.fascinated.model.server;
import cc.fascinated.common.JavaMinecraftVersion;
import cc.fascinated.config.Config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
/** /**
* @author Braydon * @author Braydon
*/ */
@Setter @Getter
public final class JavaMinecraftServer extends MinecraftServer { public final class JavaMinecraftServer extends MinecraftServer {
public JavaMinecraftServer(String hostname, String ip, int port, String motd) {
/**
* The version of the server.
*/
@NonNull private final Version version;
/**
* The players on the server.
*/
private Players players;
/**
* The favicon of the server.
*/
private Favicon favicon;
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, @NonNull Version version, Players players, Favicon favicon) {
super(hostname, ip, port, motd); super(hostname, ip, port, motd);
this.version = version;
this.players = players;
this.favicon = favicon;
}
@AllArgsConstructor @Getter
public static class Version {
/**
* The version name of the server.
*/
@NonNull
private final String name;
/**
* The server platform.
*/
private String platform;
/**
* The protocol version.
*/
private final int protocol;
/**
* The name of the protocol, null if not found.
*/
private final String protocolName;
/**
* Create a more detailed
* copy of this object.
*
* @return the detailed copy
*/
@NonNull
public Version detailedCopy() {
String platform = null;
if (name.contains(" ")) { // Parse the server platform
String[] split = name.split(" ");
if (split.length == 2) {
platform = split[0];
}
}
JavaMinecraftVersion minecraftVersion = JavaMinecraftVersion.byProtocol(protocol);
return new Version(name, platform, protocol, minecraftVersion == null ? null : minecraftVersion.getName());
}
}
@Getter @AllArgsConstructor
public static class Players {
/**
* The maximum amount of players the server can hold.
*/
private final int max;
/**
* The amount of players currently online.
*/
private final int online;
/**
* The sample of players currently online.
*/
private final String[] sample;
}
@Getter @AllArgsConstructor
public static class Favicon {
/**
* The raw base64 of the favicon.
*/
private final String base64;
/**
* The url to the favicon.
*/
private String url;
/**
* Create a new favicon for a server.
*
* @param base64 the base64 of the favicon
* @param address the address of the server
* @return the new favicon
*/
public static Favicon create(String base64, @NonNull String address) {
if (base64 == null) { // The server doesn't have a favicon
return null;
}
return new Favicon(base64, Config.INSTANCE.getWebPublicUrl() + "/server/icon/%s".formatted(address));
}
} }
} }

@ -1,5 +1,6 @@
package cc.fascinated.model.server; package cc.fascinated.model.server;
import cc.fascinated.common.ColorUtils;
import cc.fascinated.service.pinger.MinecraftServerPinger; import cc.fascinated.service.pinger.MinecraftServerPinger;
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger; import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
import io.micrometer.common.lang.NonNull; import io.micrometer.common.lang.NonNull;
@ -7,6 +8,8 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import java.util.Arrays;
/** /**
* @author Braydon * @author Braydon
*/ */
@ -15,7 +18,7 @@ public class MinecraftServer {
private final String hostname; private final String hostname;
private final String ip; private final String ip;
private final int port; private final int port;
private final String motd; private final MOTD motd;
/** /**
* A platform a Minecraft * A platform a Minecraft
@ -39,4 +42,39 @@ public class MinecraftServer {
*/ */
private final int defaultPort; private final int defaultPort;
} }
@AllArgsConstructor @Getter
public static class MOTD {
/**
* The raw motd lines
*/
private final String[] raw;
/**
* The clean motd lines
*/
private final String[] clean;
/**
* The html motd lines
*/
private final String[] html;
/**
* Create a new MOTD from a raw string.
*
* @param raw the raw motd string
* @return the new motd
*/
@NonNull
public static MOTD create(@NonNull String raw) {
String[] rawLines = raw.split("\n"); // The raw lines
return new MOTD(
rawLines,
Arrays.stream(rawLines).map(ColorUtils::stripColor).toArray(String[]::new),
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new)
);
}
}
} }

@ -1,24 +1,24 @@
package cc.fascinated.service; package cc.fascinated.service;
import cc.fascinated.EnumUtils; import cc.fascinated.common.EnumUtils;
import cc.fascinated.common.DNSUtils; import cc.fascinated.common.DNSUtils;
import cc.fascinated.common.WebRequest;
import cc.fascinated.exception.impl.BadRequestException; import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.exception.impl.ResourceNotFoundException;
import cc.fascinated.model.cache.CachedMinecraftServer; import cc.fascinated.model.cache.CachedMinecraftServer;
import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.server.JavaMinecraftServer;
import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.model.server.MinecraftServer; import cc.fascinated.model.server.MinecraftServer;
import cc.fascinated.repository.MinecraftServerCacheRepository; import cc.fascinated.repository.MinecraftServerCacheRepository;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Base64;
import java.util.Optional; import java.util.Optional;
@Service @Log4j2 @Service @Log4j2
public class ServerService { public class ServerService {
private static final String DEFAULT_SERVER_ICON = "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";
private final MinecraftServerCacheRepository serverCacheRepository; private final MinecraftServerCacheRepository serverCacheRepository;
@ -62,4 +62,28 @@ public class ServerService {
server.setCached(-1); // Indicate that the server is not cached server.setCached(-1); // Indicate that the server is not cached
return server; return server;
} }
/**
* Gets the server favicon.
*
* @param hostname the hostname of the server
* @param port the port of the server
* @return the server favicon, null if not found
*/
public byte[] getServerFavicon(String hostname, int port) {
String icon = null; // The server base64 icon
try {
JavaMinecraftServer.Favicon favicon = ((JavaMinecraftServer) getServer(MinecraftServer.Platform.JAVA.name(), hostname, port).getValue()).getFavicon();
if (favicon != null) { // Use the server's favicon
icon = favicon.getBase64();
icon = icon.substring(icon.indexOf(",") + 1); // Remove the data type from the server icon
}
} catch (BadRequestException | ResourceNotFoundException ignored) {
// Safely ignore these, we will use the default server icon
}
if (icon == null) { // Use the default server icon
icon = DEFAULT_SERVER_ICON;
}
return Base64.getDecoder().decode(icon); // Return the decoded favicon
}
} }

@ -2,12 +2,14 @@ package cc.fascinated.service.pinger.impl;
import cc.fascinated.Main; import cc.fascinated.Main;
import cc.fascinated.common.DNSUtils; import cc.fascinated.common.DNSUtils;
import cc.fascinated.common.ServerUtils;
import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol; import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart; import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart;
import cc.fascinated.exception.impl.BadRequestException; import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.exception.impl.ResourceNotFoundException; import cc.fascinated.exception.impl.ResourceNotFoundException;
import cc.fascinated.model.mojang.JavaServerStatusToken; import cc.fascinated.model.mojang.JavaServerStatusToken;
import cc.fascinated.model.server.JavaMinecraftServer; import cc.fascinated.model.server.JavaMinecraftServer;
import cc.fascinated.model.server.MinecraftServer;
import cc.fascinated.service.pinger.MinecraftServerPinger; import cc.fascinated.service.pinger.MinecraftServerPinger;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
@ -48,8 +50,17 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
// Send the status request to the server, and await back the response // Send the status request to the server, and await back the response
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart(); JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream); packetStatusInStart.process(inputStream, outputStream);
System.out.println(packetStatusInStart.getResponse());
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class); JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
return new JavaMinecraftServer(hostname, ip, port, token.getDescription()); return new JavaMinecraftServer(
hostname,
ip,
port,
MinecraftServer.MOTD.create(token.getDescription()),
token.getVersion().detailedCopy(),
token.getPlayers(),
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port))
);
} }
} catch (IOException ex) { } catch (IOException ex) {
if (ex instanceof UnknownHostException) { if (ex instanceof UnknownHostException) {

@ -21,6 +21,7 @@
<div class="flex flex-col mt-3"> <div class="flex flex-col mt-3">
<p>Player Data: <a class="text-blue-600" target=”_blank” th:href="${player_example_url}" th:text="${player_example_url}">???</a></p> <p>Player Data: <a class="text-blue-600" target=”_blank” th:href="${player_example_url}" th:text="${player_example_url}">???</a></p>
<p>Java Server: <a class="text-blue-600" target=”_blank” th:href="${java_server_example_url}" th:text="${java_server_example_url}">???</a></p>
</div> </div>
</body> </body>
</html> </html>