rename the package
This commit is contained in:
59
src/main/java/xyz/mcutils/backend/common/CachedResponse.java
Normal file
59
src/main/java/xyz/mcutils/backend/common/CachedResponse.java
Normal file
@ -0,0 +1,59 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@AllArgsConstructor @NoArgsConstructor
|
||||
@Getter
|
||||
public class CachedResponse {
|
||||
|
||||
/**
|
||||
* The cache information for this response.
|
||||
*/
|
||||
private Cache cache;
|
||||
|
||||
@AllArgsConstructor @Getter @Setter
|
||||
public static class Cache {
|
||||
/**
|
||||
* Whether this request is cached.
|
||||
*/
|
||||
private boolean cached;
|
||||
|
||||
/**
|
||||
* The unix timestamp of when this was cached.
|
||||
*/
|
||||
private long cachedTime;
|
||||
|
||||
/**
|
||||
* Create a new cache information object with the default values.
|
||||
* <p>
|
||||
* The default values are:
|
||||
* <br>
|
||||
* <ul>
|
||||
* <li>cached: true</li>
|
||||
* <li>cachedAt: {@link System#currentTimeMillis()}</li>
|
||||
* </ul>
|
||||
* <br>
|
||||
* </p>
|
||||
*
|
||||
* @return the default cache information object
|
||||
*/
|
||||
public static Cache defaultCache() {
|
||||
return new Cache(true, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if this request is cached.
|
||||
*
|
||||
* @param cached the new value of if this request is cached
|
||||
*/
|
||||
public void setCached(boolean cached) {
|
||||
this.cached = cached;
|
||||
if (!cached) {
|
||||
cachedTime = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
src/main/java/xyz/mcutils/backend/common/ColorUtils.java
Normal file
81
src/main/java/xyz/mcutils/backend/common/ColorUtils.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
58
src/main/java/xyz/mcutils/backend/common/DNSUtils.java
Normal file
58
src/main/java/xyz/mcutils/backend/common/DNSUtils.java
Normal file
@ -0,0 +1,58 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import cc.fascinated.model.dns.impl.ARecord;
|
||||
import cc.fascinated.model.dns.impl.SRVRecord;
|
||||
import lombok.NonNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.xbill.DNS.Lookup;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class DNSUtils {
|
||||
private static final String SRV_QUERY_PREFIX = "_minecraft._tcp.%s";
|
||||
|
||||
/**
|
||||
* Get the resolved address and port of the
|
||||
* given hostname by resolving the SRV records.
|
||||
*
|
||||
* @param hostname the hostname to resolve
|
||||
* @return the resolved address and port, null if none
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static SRVRecord resolveSRV(@NonNull String hostname) {
|
||||
Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records
|
||||
if (records == null) { // No records exist
|
||||
return null;
|
||||
}
|
||||
SRVRecord result = null;
|
||||
for (Record record : records) {
|
||||
result = new SRVRecord((org.xbill.DNS.SRVRecord) record);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved address of the given
|
||||
* hostname by resolving the A records.
|
||||
*
|
||||
* @param hostname the hostname to resolve
|
||||
* @return the resolved address, null if none
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static ARecord resolveA(@NonNull String hostname) {
|
||||
Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records
|
||||
if (records == null) { // No records exist
|
||||
return null;
|
||||
}
|
||||
ARecord result = null;
|
||||
for (Record record : records) {
|
||||
result = new ARecord((org.xbill.DNS.ARecord) record);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
21
src/main/java/xyz/mcutils/backend/common/EndpointStatus.java
Normal file
21
src/main/java/xyz/mcutils/backend/common/EndpointStatus.java
Normal file
@ -0,0 +1,21 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor @Getter
|
||||
public class EndpointStatus {
|
||||
|
||||
/**
|
||||
* The endpoint.
|
||||
*/
|
||||
private final String endpoint;
|
||||
|
||||
/**
|
||||
* The statuses that indicate that the endpoint is online.
|
||||
*/
|
||||
private final List<HttpStatusCode> allowedStatuses;
|
||||
}
|
26
src/main/java/xyz/mcutils/backend/common/EnumUtils.java
Normal file
26
src/main/java/xyz/mcutils/backend/common/EnumUtils.java
Normal file
@ -0,0 +1,26 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
/**
|
||||
* @author Braydon
|
||||
*/
|
||||
@UtilityClass
|
||||
public final class EnumUtils {
|
||||
/**
|
||||
* Get the enum constant of the specified enum type with the specified name.
|
||||
*
|
||||
* @param enumType the enum type
|
||||
* @param name the name of the constant to return
|
||||
* @param <T> the type of the enum
|
||||
* @return the enum constant of the specified enum type with the specified name
|
||||
*/
|
||||
public <T extends Enum<T>> T getEnumConstant(@NonNull Class<T> enumType, @NonNull String name) {
|
||||
try {
|
||||
return Enum.valueOf(enumType, name);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
132
src/main/java/xyz/mcutils/backend/common/ExpiringSet.java
Normal file
132
src/main/java/xyz/mcutils/backend/common/ExpiringSet.java
Normal file
@ -0,0 +1,132 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.NonNull;
|
||||
import net.jodah.expiringmap.ExpirationPolicy;
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A simple set that expires elements after a certain
|
||||
* amount of time, utilizing the {@link ExpiringMap} library.
|
||||
*
|
||||
* @param <T> The type of element to store within this set
|
||||
* @author Braydon
|
||||
*/
|
||||
public final class ExpiringSet<T> implements Iterable<T> {
|
||||
/**
|
||||
* The internal cache for this set.
|
||||
*/
|
||||
@NonNull private final ExpiringMap<T, Long> cache;
|
||||
|
||||
/**
|
||||
* The lifetime (in millis) of the elements in this set.
|
||||
*/
|
||||
private final long lifetime;
|
||||
|
||||
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit) {
|
||||
this(expirationPolicy, duration, timeUnit, ignored -> {});
|
||||
}
|
||||
|
||||
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit, @NonNull Consumer<T> onExpire) {
|
||||
//noinspection unchecked
|
||||
this.cache = ExpiringMap.builder()
|
||||
.expirationPolicy(expirationPolicy)
|
||||
.expiration(duration, timeUnit)
|
||||
.expirationListener((key, ignored) -> onExpire.accept((T) key))
|
||||
.build();
|
||||
this.lifetime = timeUnit.toMillis(duration); // Get the lifetime in millis
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an element to this set.
|
||||
*
|
||||
* @param element the element
|
||||
* @return whether the element was added
|
||||
*/
|
||||
public boolean add(@NonNull T element) {
|
||||
boolean contains = contains(element); // Does this set already contain the element?
|
||||
this.cache.put(element, System.currentTimeMillis() + this.lifetime);
|
||||
return !contains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry time of an element in this set.
|
||||
*
|
||||
* @param element the element
|
||||
* @return the entry time, -1 if not contained
|
||||
*/
|
||||
public long getEntryTime(@NonNull T element) {
|
||||
return contains(element) ? this.cache.get(element) - this.lifetime : -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element is
|
||||
* contained within this set.
|
||||
*
|
||||
* @param element the element
|
||||
* @return whether the element is contained
|
||||
*/
|
||||
public boolean contains(@NonNull T element) {
|
||||
Long timeout = this.cache.get(element); // Get the timeout for the element
|
||||
return timeout != null && (timeout > System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this set is empty.
|
||||
*
|
||||
* @return whether this set is empty
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return this.cache.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of this set.
|
||||
*
|
||||
* @return the size
|
||||
*/
|
||||
public int size() {
|
||||
return this.cache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an element from this set.
|
||||
*
|
||||
* @param element the element
|
||||
* @return whether the element was removed
|
||||
*/
|
||||
public boolean remove(@NonNull T element) {
|
||||
return this.cache.remove(element) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear this set.
|
||||
*/
|
||||
public void clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the elements in this set.
|
||||
*
|
||||
* @return the elements
|
||||
*/
|
||||
@NonNull
|
||||
public Set<T> getElements() {
|
||||
return this.cache.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over elements of type {@code T}.
|
||||
*
|
||||
* @return an Iterator.
|
||||
*/
|
||||
@Override @NonNull
|
||||
public Iterator<T> iterator() {
|
||||
return this.cache.keySet().iterator();
|
||||
}
|
||||
}
|
42
src/main/java/xyz/mcutils/backend/common/IPUtils.java
Normal file
42
src/main/java/xyz/mcutils/backend/common/IPUtils.java
Normal file
@ -0,0 +1,42 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class IPUtils {
|
||||
/**
|
||||
* The headers that contain the IP.
|
||||
*/
|
||||
private static final String[] IP_HEADERS = new String[] {
|
||||
"CF-Connecting-IP",
|
||||
"X-Forwarded-For"
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the real IP from the given request.
|
||||
*
|
||||
* @param request the request
|
||||
* @return the real IP
|
||||
*/
|
||||
public static String getRealIp(HttpServletRequest request) {
|
||||
String ip = request.getRemoteAddr();
|
||||
for (String headerName : IP_HEADERS) {
|
||||
String header = request.getHeader(headerName);
|
||||
if (header == null) {
|
||||
continue;
|
||||
}
|
||||
if (!header.contains(",")) { // Handle single IP
|
||||
ip = header;
|
||||
break;
|
||||
}
|
||||
// Handle multiple IPs
|
||||
String[] ips = header.split(",");
|
||||
for (String ipHeader : ips) {
|
||||
ip = ipHeader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
59
src/main/java/xyz/mcutils/backend/common/ImageUtils.java
Normal file
59
src/main/java/xyz/mcutils/backend/common/ImageUtils.java
Normal file
@ -0,0 +1,59 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@Log4j2
|
||||
public class ImageUtils {
|
||||
/**
|
||||
* Scale the given image to the provided size.
|
||||
*
|
||||
* @param image the image to scale
|
||||
* @param size the size 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);
|
||||
Graphics2D graphics = scaled.createGraphics();
|
||||
graphics.drawImage(image, AffineTransform.getScaleInstance(size, size), null);
|
||||
graphics.dispose();
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the given image.
|
||||
*
|
||||
* @param image the image to flip
|
||||
* @return the flipped image
|
||||
*/
|
||||
public static BufferedImage flip(@NotNull final BufferedImage image) {
|
||||
BufferedImage flipped = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = flipped.createGraphics();
|
||||
graphics.drawImage(image, image.getWidth(), 0, 0, image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), null);
|
||||
graphics.dispose();
|
||||
return flipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an image to bytes.
|
||||
*
|
||||
* @param image the image to convert
|
||||
* @return the image as bytes
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static byte[] imageToBytes(BufferedImage image) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, "png", outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Failed to convert image to bytes", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
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
|
||||
|
||||
V1_7_6(5, "v1_7_R4"), // 1.7.6 - 1.7.10
|
||||
|
||||
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;
|
||||
|
||||
private static final JavaMinecraftVersion[] VALUES = JavaMinecraftVersion.values();
|
||||
|
||||
/**
|
||||
* 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 minimum Minecraft version.
|
||||
*
|
||||
* @return the minimum version
|
||||
*/
|
||||
@NonNull
|
||||
public static JavaMinecraftVersion getMinimumVersion() {
|
||||
return VALUES[VALUES.length - 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
50
src/main/java/xyz/mcutils/backend/common/PlayerUtils.java
Normal file
50
src/main/java/xyz/mcutils/backend/common/PlayerUtils.java
Normal file
@ -0,0 +1,50 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import xyz.mcutils.backend.Main;
|
||||
import cc.fascinated.exception.impl.BadRequestException;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass @Log4j2
|
||||
public class PlayerUtils {
|
||||
|
||||
/**
|
||||
* Gets the UUID from the string.
|
||||
*
|
||||
* @param id the id string
|
||||
* @return the UUID
|
||||
*/
|
||||
public static UUID getUuidFromString(String id) {
|
||||
UUID uuid;
|
||||
boolean isFullUuid = id.length() == 36;
|
||||
if (id.length() == 32 || isFullUuid) {
|
||||
try {
|
||||
uuid = isFullUuid ? UUID.fromString(id) : UUIDUtils.addDashes(id);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new BadRequestException("Invalid UUID provided: %s".formatted(id));
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the skin data from the URL.
|
||||
*
|
||||
* @return the skin data
|
||||
*/
|
||||
@SneakyThrows
|
||||
@JsonIgnore
|
||||
public static byte[] getSkinImage(String url) {
|
||||
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
|
||||
HttpResponse.BodyHandlers.ofByteArray());
|
||||
return response.body();
|
||||
}
|
||||
}
|
16
src/main/java/xyz/mcutils/backend/common/ServerUtils.java
Normal file
16
src/main/java/xyz/mcutils/backend/common/ServerUtils.java
Normal file
@ -0,0 +1,16 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ServerUtils {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
18
src/main/java/xyz/mcutils/backend/common/Tuple.java
Normal file
18
src/main/java/xyz/mcutils/backend/common/Tuple.java
Normal file
@ -0,0 +1,18 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter @AllArgsConstructor
|
||||
public class Tuple<L, R> {
|
||||
|
||||
/**
|
||||
* The left value of the tuple.
|
||||
*/
|
||||
private final L left;
|
||||
|
||||
/**
|
||||
* The right value of the tuple.
|
||||
*/
|
||||
private final R right;
|
||||
}
|
36
src/main/java/xyz/mcutils/backend/common/UUIDUtils.java
Normal file
36
src/main/java/xyz/mcutils/backend/common/UUIDUtils.java
Normal file
@ -0,0 +1,36 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import io.micrometer.common.lang.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass
|
||||
public class UUIDUtils {
|
||||
|
||||
/**
|
||||
* Add dashes to a UUID.
|
||||
*
|
||||
* @param trimmed the UUID without dashes
|
||||
* @return the UUID with dashes
|
||||
*/
|
||||
@NonNull
|
||||
public static UUID addDashes(@NonNull String trimmed) {
|
||||
StringBuilder builder = new StringBuilder(trimmed);
|
||||
for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
|
||||
builder.insert(pos, "-");
|
||||
}
|
||||
return UUID.fromString(builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove dashes from a UUID.
|
||||
*
|
||||
* @param dashed the UUID with dashes
|
||||
* @return the UUID without dashes
|
||||
*/
|
||||
@NonNull
|
||||
public static String removeDashes(@NonNull UUID dashed) {
|
||||
return dashed.toString().replace("-", "");
|
||||
}
|
||||
}
|
77
src/main/java/xyz/mcutils/backend/common/WebRequest.java
Normal file
77
src/main/java/xyz/mcutils/backend/common/WebRequest.java
Normal file
@ -0,0 +1,77 @@
|
||||
package cc.fascinated.common;
|
||||
|
||||
import cc.fascinated.exception.impl.RateLimitException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@UtilityClass
|
||||
public class WebRequest {
|
||||
|
||||
/**
|
||||
* The web client.
|
||||
*/
|
||||
private static final RestClient CLIENT;
|
||||
|
||||
static {
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(2500); // 2.5 seconds
|
||||
CLIENT = RestClient.builder()
|
||||
.requestFactory(requestFactory)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
* @param <T> the type of the response
|
||||
*/
|
||||
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
|
||||
ResponseEntity<T> responseEntity = CLIENT.get()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
|
||||
if (responseEntity.getStatusCode().isError()) {
|
||||
return null;
|
||||
}
|
||||
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
|
||||
throw new RateLimitException("Rate limit reached");
|
||||
}
|
||||
return responseEntity.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
*/
|
||||
public static ResponseEntity<?> get(String url, Class<?> clazz) {
|
||||
return CLIENT.get()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response from the given URL.
|
||||
*
|
||||
* @param url the url
|
||||
* @return the response
|
||||
*/
|
||||
public static ResponseEntity<?> head(String url, Class<?> clazz) {
|
||||
return CLIENT.head()
|
||||
.uri(url)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||
.toEntity(clazz);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package cc.fascinated.common.packet;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramSocket;
|
||||
|
||||
/**
|
||||
* Represents a packet in the
|
||||
* Minecraft Bedrock protocol.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
|
||||
*/
|
||||
public interface MinecraftBedrockPacket {
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param socket the socket to process the packet for
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
void process(@NonNull DatagramSocket socket) throws IOException;
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package cc.fascinated.common.packet;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Represents a packet in the
|
||||
* Minecraft Java protocol.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Protocol">Protocol Docs</a>
|
||||
*/
|
||||
public abstract class MinecraftJavaPacket {
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param inputStream the input stream to read from
|
||||
* @param outputStream the output stream to write to
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public abstract void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException;
|
||||
|
||||
/**
|
||||
* Write a variable integer to the output stream.
|
||||
*
|
||||
* @param outputStream the output stream to write to
|
||||
* @param paramInt the integer to write
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
protected final void writeVarInt(DataOutputStream outputStream, int paramInt) throws IOException {
|
||||
while (true) {
|
||||
if ((paramInt & 0xFFFFFF80) == 0) {
|
||||
outputStream.writeByte(paramInt);
|
||||
return;
|
||||
}
|
||||
outputStream.writeByte(paramInt & 0x7F | 0x80);
|
||||
paramInt >>>= 7;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a variable integer from the input stream.
|
||||
*
|
||||
* @param inputStream the input stream to read from
|
||||
* @return the integer that was read
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
protected final int readVarInt(@NonNull DataInputStream inputStream) throws IOException {
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (true) {
|
||||
int k = inputStream.readByte();
|
||||
i |= (k & 0x7F) << j++ * 7;
|
||||
if (j > 5) {
|
||||
throw new RuntimeException("VarInt too big");
|
||||
}
|
||||
if ((k & 0x80) != 128) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package cc.fascinated.common.packet.impl.bedrock;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftBedrockPacket;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* This packet is sent by the client to the server to
|
||||
* request a pong response from the server. The server
|
||||
* will respond with a string containing the server's status.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
|
||||
*/
|
||||
public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
|
||||
private static final byte ID = 0x01; // The ID of the packet
|
||||
private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };
|
||||
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param socket the socket to process the packet for
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void process(@NonNull DatagramSocket socket) throws IOException {
|
||||
// Construct the packet buffer
|
||||
ByteBuffer buffer = ByteBuffer.allocate(33).order(ByteOrder.LITTLE_ENDIAN);;
|
||||
buffer.put(ID); // Packet ID
|
||||
buffer.putLong(System.currentTimeMillis()); // Timestamp
|
||||
buffer.put(MAGIC); // Magic
|
||||
buffer.putLong(0L); // Client GUID
|
||||
|
||||
// Send the packet
|
||||
socket.send(new DatagramPacket(buffer.array(), 0, buffer.limit()));
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package cc.fascinated.common.packet.impl.bedrock;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftBedrockPacket;
|
||||
import cc.fascinated.model.server.BedrockMinecraftServer;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* This packet is sent by the server to the client in
|
||||
* response to the {@link BedrockPacketUnconnectedPing}.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
|
||||
*/
|
||||
@Getter
|
||||
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
|
||||
private static final byte ID = 0x1C; // The ID of the packet
|
||||
|
||||
/**
|
||||
* The response from the server, null if none.
|
||||
*/
|
||||
private String response;
|
||||
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param socket the socket to process the packet for
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void process(@NonNull DatagramSocket socket) throws IOException {
|
||||
// Handle receiving of the packet
|
||||
byte[] receiveData = new byte[2048];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||
socket.receive(receivePacket);
|
||||
|
||||
// Construct a buffer from the received packet
|
||||
ByteBuffer buffer = ByteBuffer.wrap(receivePacket.getData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
byte id = buffer.get(); // The received packet id
|
||||
if (id == ID) {
|
||||
String response = new String(buffer.array(), StandardCharsets.UTF_8).trim(); // Extract the response
|
||||
|
||||
// Trim the length of the response (short) from the
|
||||
// start of the string, which begins with the edition name
|
||||
for (BedrockMinecraftServer.Edition edition : BedrockMinecraftServer.Edition.values()) {
|
||||
int startIndex = response.indexOf(edition.name());
|
||||
if (startIndex != -1) {
|
||||
response = response.substring(startIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package cc.fascinated.common.packet.impl.java;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This packet is sent by the client to the server to set
|
||||
* the hostname, port, and protocol version of the client.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
|
||||
*/
|
||||
@AllArgsConstructor @ToString
|
||||
public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket {
|
||||
private static final byte ID = 0x00; // The ID of the packet
|
||||
private static final int STATUS_HANDSHAKE = 1; // The status handshake ID
|
||||
|
||||
/**
|
||||
* The hostname of the server.
|
||||
*/
|
||||
@NonNull private final String hostname;
|
||||
|
||||
/**
|
||||
* The port of the server.
|
||||
*/
|
||||
private final int port;
|
||||
|
||||
/**
|
||||
* The protocol version of the server.
|
||||
*/
|
||||
private final int protocolVersion;
|
||||
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param inputStream the input stream to read from
|
||||
* @param outputStream the output stream to write to
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
|
||||
try (ByteArrayOutputStream handshakeBytes = new ByteArrayOutputStream();
|
||||
DataOutputStream handshake = new DataOutputStream(handshakeBytes)
|
||||
) {
|
||||
handshake.writeByte(ID); // Write the ID of the packet
|
||||
writeVarInt(handshake, protocolVersion); // Write the protocol version
|
||||
writeVarInt(handshake, hostname.length()); // Write the length of the hostname
|
||||
handshake.writeBytes(hostname); // Write the hostname
|
||||
handshake.writeShort(port); // Write the port
|
||||
writeVarInt(handshake, STATUS_HANDSHAKE); // Write the status handshake ID
|
||||
|
||||
// Write the handshake bytes to the output stream
|
||||
writeVarInt(outputStream, handshakeBytes.size());
|
||||
outputStream.write(handshakeBytes.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package cc.fascinated.common.packet.impl.java;
|
||||
|
||||
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This packet is sent by the client to the server to request the
|
||||
* status of the server. The server will respond with a json object
|
||||
* containing the server's status.
|
||||
*
|
||||
* @author Braydon
|
||||
* @see <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
|
||||
*/
|
||||
@Getter
|
||||
public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
|
||||
private static final byte ID = 0x00; // The ID of the packet
|
||||
|
||||
/**
|
||||
* The response json from the server, null if none.
|
||||
*/
|
||||
private String response;
|
||||
|
||||
/**
|
||||
* Process this packet.
|
||||
*
|
||||
* @param inputStream the input stream to read from
|
||||
* @param outputStream the output stream to write to
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
|
||||
// Send the status request
|
||||
outputStream.writeByte(0x01); // Size of packet
|
||||
outputStream.writeByte(ID);
|
||||
|
||||
// Read the status response
|
||||
readVarInt(inputStream); // Size of the response
|
||||
int id = readVarInt(inputStream);
|
||||
if (id == -1) { // The stream was prematurely ended
|
||||
throw new IOException("Server prematurely ended stream.");
|
||||
} else if (id != ID) { // Invalid packet ID
|
||||
throw new IOException("Server returned invalid packet ID.");
|
||||
}
|
||||
|
||||
int length = readVarInt(inputStream); // Length of the response
|
||||
if (length == -1) { // The stream was prematurely ended
|
||||
throw new IOException("Server prematurely ended stream.");
|
||||
} else if (length == 0) {
|
||||
throw new IOException("Server returned unexpected value.");
|
||||
}
|
||||
|
||||
// Get the json response
|
||||
byte[] data = new byte[length];
|
||||
inputStream.readFully(data);
|
||||
response = new String(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cc.fascinated.common.renderer;
|
||||
|
||||
import cc.fascinated.model.skin.ISkinPart;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public abstract class IsometricSkinRenderer<T extends ISkinPart> extends SkinRenderer<T> {
|
||||
|
||||
/**
|
||||
* Draw a part onto the texture.
|
||||
*
|
||||
* @param graphics the graphics to draw to
|
||||
* @param partImage the part image to draw
|
||||
* @param transform the transform to apply
|
||||
* @param x the x position to draw at
|
||||
* @param y the y position to draw at
|
||||
* @param width the part image width
|
||||
* @param height the part image height
|
||||
*/
|
||||
protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform,
|
||||
double x, double y, int width, int height) {
|
||||
graphics.setTransform(transform);
|
||||
graphics.drawImage(partImage, (int) x, (int) y, width, height, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package cc.fascinated.common.renderer;
|
||||
|
||||
import cc.fascinated.common.ImageUtils;
|
||||
import cc.fascinated.model.skin.ISkinPart;
|
||||
import cc.fascinated.model.skin.Skin;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
@Log4j2
|
||||
public abstract class SkinRenderer<T extends ISkinPart> {
|
||||
|
||||
/**
|
||||
* Get the texture of a part of the skin.
|
||||
*
|
||||
* @param skin the skin to get the part texture from
|
||||
* @param part the part of the skin to get
|
||||
* @param size the size to scale the texture to
|
||||
* @param renderOverlays should the overlays be rendered
|
||||
* @return the texture of the skin part
|
||||
*/
|
||||
@SneakyThrows
|
||||
public BufferedImage getVanillaSkinPart(Skin skin, ISkinPart.Vanilla part, double size, boolean renderOverlays) {
|
||||
ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part
|
||||
|
||||
// The skin texture is legacy, use legacy coordinates
|
||||
if (skin.isLegacy() && part.hasLegacyCoordinates()) {
|
||||
coordinates = part.getLegacyCoordinates();
|
||||
}
|
||||
int width = part.getWidth(); // The width of the part
|
||||
if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) {
|
||||
width--;
|
||||
}
|
||||
BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture
|
||||
BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size);
|
||||
if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) {
|
||||
partTexture = ImageUtils.flip(partTexture);
|
||||
}
|
||||
|
||||
// Draw part overlays
|
||||
ISkinPart.Vanilla[] overlayParts = part.getOverlays();
|
||||
if (overlayParts != null && renderOverlays) {
|
||||
log.info("Applying overlays to part: {}", part.name());
|
||||
for (ISkinPart.Vanilla overlay : overlayParts) {
|
||||
applyOverlay(partTexture.createGraphics(), getVanillaSkinPart(skin, overlay, size, false));
|
||||
}
|
||||
}
|
||||
|
||||
return partTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the texture of a specific part of the skin.
|
||||
*
|
||||
* @param skinImage the skin image to get the part from
|
||||
* @param x the x position of the part
|
||||
* @param y the y position of the part
|
||||
* @param width the width of the part
|
||||
* @param height the height of the part
|
||||
* @param size the size to scale the part to
|
||||
* @return the texture of the skin part
|
||||
*/
|
||||
@SneakyThrows
|
||||
private BufferedImage getSkinPartTexture(BufferedImage skinImage, int x, int y, int width, int height, double size) {
|
||||
// Create a new BufferedImage for the part of the skin texture
|
||||
BufferedImage headTexture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
// Crop just the part we want based on our x, y, width, and height
|
||||
headTexture.getGraphics().drawImage(skinImage, 0, 0, width, height, x, y, x + width, y + height, null);
|
||||
|
||||
// Scale the skin part texture
|
||||
if (size > 0D) {
|
||||
headTexture = ImageUtils.resize(headTexture, size);
|
||||
}
|
||||
return headTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an overlay to a texture.
|
||||
*
|
||||
* @param graphics the graphics to overlay on
|
||||
* @param overlayImage the part to overlay
|
||||
*/
|
||||
protected void applyOverlay(Graphics2D graphics, BufferedImage overlayImage) {
|
||||
try {
|
||||
graphics.drawImage(overlayImage, 0, 0, null);
|
||||
graphics.dispose();
|
||||
} catch (Exception ignored) {
|
||||
// We can safely ignore this, legacy
|
||||
// skins don't have overlays
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the skin part for the player's skin.
|
||||
*
|
||||
* @param skin the player's skin
|
||||
* @param part the skin part to render
|
||||
* @param renderOverlays should the overlays be rendered
|
||||
* @param size the size of the part
|
||||
* @return the rendered skin part
|
||||
*/
|
||||
public abstract BufferedImage render(Skin skin, T part, boolean renderOverlays, int size);
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package cc.fascinated.common.renderer.impl;
|
||||
|
||||
import cc.fascinated.common.ImageUtils;
|
||||
import cc.fascinated.common.renderer.SkinRenderer;
|
||||
import cc.fascinated.model.skin.ISkinPart;
|
||||
import cc.fascinated.model.skin.Skin;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@AllArgsConstructor @Getter @Log4j2
|
||||
public class BodyRenderer extends SkinRenderer<ISkinPart.Custom> {
|
||||
public static final BodyRenderer INSTANCE = new BodyRenderer();
|
||||
|
||||
@Override
|
||||
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
|
||||
BufferedImage texture = new BufferedImage(16, 32, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||
|
||||
// Get the Vanilla skin parts to draw
|
||||
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, -1, renderOverlays);
|
||||
BufferedImage body = getVanillaSkinPart(skin, ISkinPart.Vanilla.BODY_FRONT, -1, renderOverlays);
|
||||
BufferedImage leftArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_ARM_FRONT, -1, renderOverlays);
|
||||
BufferedImage rightArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_ARM_FRONT, -1, renderOverlays);
|
||||
BufferedImage leftLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_LEG_FRONT, -1, renderOverlays);
|
||||
BufferedImage rightLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_LEG_FRONT, -1, renderOverlays);
|
||||
|
||||
// Draw the body parts
|
||||
graphics.drawImage(face, 4, 0, null);
|
||||
graphics.drawImage(body, 4, 8, null);
|
||||
graphics.drawImage(leftArm, skin.getModel() == Skin.Model.SLIM ? 1 : 0, 8, null);
|
||||
graphics.drawImage(rightArm, 12, 8, null);
|
||||
graphics.drawImage(leftLeg, 8, 20, null);
|
||||
graphics.drawImage(rightLeg, 4, 20, null);
|
||||
|
||||
graphics.dispose();
|
||||
return ImageUtils.resize(texture, (double) size / 32);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package cc.fascinated.common.renderer.impl;
|
||||
|
||||
import cc.fascinated.common.renderer.IsometricSkinRenderer;
|
||||
import cc.fascinated.model.skin.ISkinPart;
|
||||
import cc.fascinated.model.skin.Skin;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class IsometricHeadRenderer extends IsometricSkinRenderer<ISkinPart.Custom> {
|
||||
public static final IsometricHeadRenderer INSTANCE = new IsometricHeadRenderer();
|
||||
|
||||
private static final double SKEW_A = 26D / 45D; // 0.57777777
|
||||
private static final double SKEW_B = SKEW_A * 2D; // 1.15555555
|
||||
|
||||
private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0);
|
||||
private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A);
|
||||
private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D);
|
||||
|
||||
@Override
|
||||
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
|
||||
double scale = (size / 8D) / 2.5;
|
||||
double zOffset = scale * 3.5D;
|
||||
double xOffset = scale * 2D;
|
||||
|
||||
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||
|
||||
// Get the Vanilla skin parts to draw
|
||||
BufferedImage headTop = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_TOP, scale, renderOverlays);
|
||||
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, scale, renderOverlays);
|
||||
BufferedImage headLeft = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_LEFT, scale, renderOverlays);
|
||||
|
||||
// Draw the top head part
|
||||
drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2);
|
||||
|
||||
// Draw the face part
|
||||
double x = xOffset + 8 * scale;
|
||||
drawPart(graphics, face, FACE_TRANSFORM, x, x + zOffset - 0.5, face.getWidth(), face.getHeight());
|
||||
|
||||
// Draw the left head part
|
||||
drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight());
|
||||
|
||||
graphics.dispose();
|
||||
return texture;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package cc.fascinated.common.renderer.impl;
|
||||
|
||||
import cc.fascinated.common.renderer.SkinRenderer;
|
||||
import cc.fascinated.model.skin.ISkinPart;
|
||||
import cc.fascinated.model.skin.Skin;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
@AllArgsConstructor @Getter @Log4j2
|
||||
public class SquareRenderer extends SkinRenderer<ISkinPart.Vanilla> {
|
||||
public static final SquareRenderer INSTANCE = new SquareRenderer();
|
||||
|
||||
@Override
|
||||
public BufferedImage render(Skin skin, ISkinPart.Vanilla part, boolean renderOverlays, int size) {
|
||||
double scale = size / 8D;
|
||||
BufferedImage partImage = getVanillaSkinPart(skin, part, scale, renderOverlays); // Get the part image
|
||||
if (!renderOverlays) { // Not rendering overlays
|
||||
return partImage;
|
||||
}
|
||||
// Create a new image, draw our skin part texture, and then apply overlays
|
||||
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||
graphics.drawImage(partImage, 0, 0, null);
|
||||
|
||||
graphics.dispose();
|
||||
return texture;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user