rename the package

This commit is contained in:
Lee
2024-04-13 20:44:05 +01:00
parent b14c969013
commit a15326a847
71 changed files with 31 additions and 31 deletions

View 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;
}
}
}
}

View 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();
}
}

View 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;
}
}

View 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;
}

View 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;
}
}
}

View 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();
}
}

View 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;
}
}

View 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);
}
}
}

View File

@ -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;
}
}

View 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();
}
}

View 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);
}
}

View 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;
}

View 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("-", "");
}
}

View 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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}