├── .gitea
└── workflows
│ └── ci.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── influx-commands.md
├── pom.xml
├── renovate.json
└── src
├── main
├── java
│ └── xyz
│ │ └── mcutils
│ │ └── backend
│ │ ├── Main.java
│ │ ├── common
│ │ ├── AppConfig.java
│ │ ├── CachedResponse.java
│ │ ├── ColorUtils.java
│ │ ├── DNSUtils.java
│ │ ├── Endpoint.java
│ │ ├── EnumUtils.java
│ │ ├── ExpiringSet.java
│ │ ├── Fonts.java
│ │ ├── IPUtils.java
│ │ ├── ImageUtils.java
│ │ ├── JavaMinecraftVersion.java
│ │ ├── MojangServer.java
│ │ ├── PlayerUtils.java
│ │ ├── ServerUtils.java
│ │ ├── Timer.java
│ │ ├── Tuple.java
│ │ ├── UUIDUtils.java
│ │ ├── WebRequest.java
│ │ ├── packet
│ │ │ ├── MinecraftBedrockPacket.java
│ │ │ ├── MinecraftJavaPacket.java
│ │ │ └── impl
│ │ │ │ ├── bedrock
│ │ │ │ ├── BedrockPacketUnconnectedPing.java
│ │ │ │ └── BedrockPacketUnconnectedPong.java
│ │ │ │ └── java
│ │ │ │ ├── JavaPacketHandshakingInSetProtocol.java
│ │ │ │ └── JavaPacketStatusInStart.java
│ │ └── renderer
│ │ │ ├── IsometricSkinRenderer.java
│ │ │ ├── Renderer.java
│ │ │ ├── SkinRenderer.java
│ │ │ └── impl
│ │ │ ├── server
│ │ │ └── ServerPreviewRenderer.java
│ │ │ └── skin
│ │ │ ├── BodyRenderer.java
│ │ │ ├── IsometricHeadRenderer.java
│ │ │ └── SquareRenderer.java
│ │ ├── config
│ │ ├── Config.java
│ │ ├── MongoConfig.java
│ │ ├── OpenAPIConfiguration.java
│ │ └── RedisConfig.java
│ │ ├── controller
│ │ ├── HealthController.java
│ │ ├── HomeController.java
│ │ ├── MojangController.java
│ │ ├── PlayerController.java
│ │ └── ServerController.java
│ │ ├── exception
│ │ ├── ExceptionControllerAdvice.java
│ │ └── impl
│ │ │ ├── BadRequestException.java
│ │ │ ├── InternalServerErrorException.java
│ │ │ ├── MojangAPIRateLimitException.java
│ │ │ ├── RateLimitException.java
│ │ │ └── ResourceNotFoundException.java
│ │ ├── log
│ │ └── TransactionLogger.java
│ │ ├── model
│ │ ├── cache
│ │ │ ├── CachedEndpointStatus.java
│ │ │ ├── CachedMinecraftServer.java
│ │ │ ├── CachedPlayer.java
│ │ │ ├── CachedPlayerName.java
│ │ │ ├── CachedPlayerSkinPart.java
│ │ │ └── CachedServerPreview.java
│ │ ├── dns
│ │ │ ├── DNSRecord.java
│ │ │ └── impl
│ │ │ │ ├── ARecord.java
│ │ │ │ └── SRVRecord.java
│ │ ├── metric
│ │ │ └── WebsocketMetrics.java
│ │ ├── mojang
│ │ │ └── EndpointStatus.java
│ │ ├── player
│ │ │ ├── Cape.java
│ │ │ └── Player.java
│ │ ├── response
│ │ │ └── ErrorResponse.java
│ │ ├── server
│ │ │ ├── BedrockMinecraftServer.java
│ │ │ ├── JavaMinecraftServer.java
│ │ │ └── MinecraftServer.java
│ │ ├── skin
│ │ │ ├── ISkinPart.java
│ │ │ └── Skin.java
│ │ └── token
│ │ │ ├── JavaServerStatusToken.java
│ │ │ ├── MojangProfileToken.java
│ │ │ └── MojangUsernameToUuidToken.java
│ │ ├── repository
│ │ ├── mongo
│ │ │ └── MetricsRepository.java
│ │ └── redis
│ │ │ ├── MinecraftServerCacheRepository.java
│ │ │ ├── PlayerCacheRepository.java
│ │ │ ├── PlayerNameCacheRepository.java
│ │ │ ├── PlayerSkinPartCacheRepository.java
│ │ │ └── ServerPreviewCacheRepository.java
│ │ ├── service
│ │ ├── MaxMindService.java
│ │ ├── MetricService.java
│ │ ├── MojangService.java
│ │ ├── PlayerService.java
│ │ ├── ServerService.java
│ │ ├── metric
│ │ │ ├── Metric.java
│ │ │ ├── impl
│ │ │ │ ├── DoubleMetric.java
│ │ │ │ ├── IntegerMetric.java
│ │ │ │ └── MapMetric.java
│ │ │ └── metrics
│ │ │ │ ├── ConnectedSocketsMetric.java
│ │ │ │ ├── RequestsPerRouteMetric.java
│ │ │ │ ├── TotalRequestsMetric.java
│ │ │ │ ├── UniquePlayerLookupsMetric.java
│ │ │ │ ├── UniqueServerLookupsMetric.java
│ │ │ │ └── process
│ │ │ │ ├── CpuUsageMetric.java
│ │ │ │ └── MemoryMetric.java
│ │ └── pinger
│ │ │ ├── MinecraftServerPinger.java
│ │ │ └── impl
│ │ │ ├── BedrockMinecraftServerPinger.java
│ │ │ └── JavaMinecraftServerPinger.java
│ │ └── websocket
│ │ ├── WebSocket.java
│ │ ├── WebSocketManager.java
│ │ └── impl
│ │ └── MetricsWebSocket.java
└── resources
│ ├── application.yml
│ ├── fonts
│ └── minecraft-font.ttf
│ ├── icons
│ ├── ping.png
│ └── server_background.png
│ ├── public
│ ├── favicon.ico
│ └── robots.txt
│ └── templates
│ └── index.html
└── test
└── java
└── xyz
└── mcutils
└── backend
└── test
├── config
└── TestRedisConfig.java
└── tests
├── MojangControllerTests.java
├── PlayerControllerTests.java
└── ServerControllerTests.java
/.gitea/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Deploy App
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | paths-ignore:
7 | - .gitignore
8 | - README.md
9 | - LICENSE
10 | - docker-compose.yml
11 |
12 | jobs:
13 | docker:
14 | strategy:
15 | matrix:
16 | arch: ["ubuntu-latest"]
17 | git-version: ["2.44.0"]
18 | java-version: ["17"]
19 | maven-version: ["3.8.5"]
20 | runs-on: ${{ matrix.arch }}
21 |
22 | # Steps to run
23 | steps:
24 | # Checkout the repo
25 | - name: Checkout
26 | uses: actions/checkout@v4
27 |
28 | # Setup Java and Maven
29 | - name: Set up JDK and Maven
30 | uses: s4u/setup-maven-action@v1.14.0
31 | with:
32 | java-version: ${{ matrix.java-version }}
33 | distribution: "zulu"
34 | maven-version: ${{ matrix.maven-version }}
35 |
36 | # Run JUnit Tests
37 | - name: Run Tests
38 | run: mvn --batch-mode test -q
39 |
40 | # Re-checkout to reset the FS before deploying to Dokku
41 | - name: Checkout - Reset FS
42 | uses: actions/checkout@v4
43 | with:
44 | fetch-depth: 0
45 |
46 | # Deploy to Dokku
47 | - name: Push to dokku
48 | uses: dokku/github-action@master
49 | with:
50 | git_remote_url: "ssh://dokku@10.0.50.137:22/minecraft-helper"
51 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### ME template
2 | *.class
3 | *.log
4 | *.ctxt
5 | .mtj.tmp/
6 | *.jar
7 | *.war
8 | *.nar
9 | *.ear
10 | *.zip
11 | *.tar.gz
12 | *.rar
13 | hs_err_pid*
14 | replay_pid*
15 | .idea
16 | cmake-build-*/
17 | .idea/**/mongoSettings.xml
18 | *.iws
19 | out/
20 | build/
21 | work/
22 | .idea_modules/
23 | atlassian-ide-plugin.xml
24 | com_crashlytics_export_strings.xml
25 | crashlytics.properties
26 | crashlytics-build.properties
27 | fabric.properties
28 | git.properties
29 | pom.xml.versionsBackup
30 | application.yml
31 | target/
32 |
33 | ### MaxMind GeoIP2
34 | data/
35 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM maven:3.9.8-eclipse-temurin-17-alpine
2 |
3 | RUN apk --update --upgrade --no-cache add fontconfig ttf-freefont font-noto terminus-font \
4 | && fc-cache -f \
5 | && fc-list | sort
6 |
7 | # Set the working directory
8 | WORKDIR /home/container
9 |
10 | # Copy the current directory contents into the container at /home/container
11 | COPY . .
12 |
13 | # Build the jar
14 | RUN mvn package -q -Dmaven.test.skip -DskipTests -T2C
15 |
16 | # Make port 80 available to the world outside this container
17 | EXPOSE 80
18 | ENV PORT=80
19 |
20 | # Indicate that we're running in production
21 | ENV ENVIRONMENT=production
22 |
23 | # Run the jar file
24 | CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024, Fascinated
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minecraft Utilities - Backend
2 |
3 | See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.
--------------------------------------------------------------------------------
/influx-commands.md:
--------------------------------------------------------------------------------
1 | # Useful InfluxDB commands
2 |
3 | ## Delete data from bucket
4 |
5 | ```bash
6 | influx delete --bucket mcutils --start 2024-01-01T00:00:00Z --stop 2025-01-05T00:00:00Z --org mcutils --token setme --predicate '_measurement="requests_per_route"
7 | ```
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | cc.fascinated
8 | Minecraft-Utilities
9 | 1.0.0
10 |
11 |
12 | 17
13 | 17
14 | UTF-8
15 |
16 |
17 |
18 | org.springframework.boot
19 | spring-boot-starter-parent
20 | 3.3.2
21 |
22 |
23 |
24 |
25 |
26 | ${project.artifactId}
27 |
28 |
29 |
30 | org.springframework.boot
31 | spring-boot-maven-plugin
32 |
33 |
34 | build-info
35 |
36 | build-info
37 |
38 |
39 |
40 | ${project.description}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | jitpack.io
53 | https://jitpack.io
54 |
55 |
56 |
57 |
58 |
59 |
60 | org.springframework.boot
61 | spring-boot-starter-web
62 |
63 |
64 |
65 |
66 | org.springframework.boot
67 | spring-boot-starter-websocket
68 |
69 |
70 |
71 |
72 | org.springframework.boot
73 | spring-boot-starter-data-redis
74 |
75 |
76 | io.lettuce
77 | lettuce-core
78 |
79 |
80 |
81 |
82 | redis.clients
83 | jedis
84 |
85 |
86 |
87 |
88 | org.springframework.boot
89 | spring-boot-starter-data-mongodb
90 |
91 |
92 |
93 |
94 | org.projectlombok
95 | lombok
96 | 1.18.34
97 | provided
98 |
99 |
100 | com.google.code.gson
101 | gson
102 | 2.11.0
103 | compile
104 |
105 |
106 | net.jodah
107 | expiringmap
108 | 0.5.11
109 | compile
110 |
111 |
112 | net.md-5
113 | bungeecord-chat
114 | 1.20-R0.2
115 | compile
116 |
117 |
118 | org.springframework.boot
119 | spring-boot-starter-thymeleaf
120 | compile
121 |
122 |
123 | org.apache.httpcomponents.client5
124 | httpclient5
125 | 5.3.1
126 |
127 |
128 | org.springframework.boot
129 | spring-boot-actuator-autoconfigure
130 |
131 |
132 |
133 |
134 | io.sentry
135 | sentry-spring-boot-starter-jakarta
136 | 7.14.0
137 |
138 |
139 |
140 |
141 | com.influxdb
142 | influxdb-spring
143 | 7.2.0
144 | compile
145 |
146 |
147 | com.influxdb
148 | influxdb-client-java
149 | 7.2.0
150 |
151 |
152 |
153 |
154 | com.github.dnsjava
155 | dnsjava
156 | v3.5.2
157 | compile
158 |
159 |
160 |
161 |
162 | org.springdoc
163 | springdoc-openapi-starter-webmvc-ui
164 | 2.6.0
165 | compile
166 |
167 |
168 |
169 |
170 | com.maxmind.geoip2
171 | geoip2
172 | 4.2.0
173 |
174 |
175 |
176 |
177 | org.codehaus.plexus
178 | plexus-archiver
179 | 4.10.0
180 |
181 |
182 |
183 |
184 | org.springframework.boot
185 | spring-boot-starter-test
186 | test
187 |
188 |
189 | com.github.codemonstur
190 | embedded-redis
191 | 1.4.3
192 | test
193 |
194 |
195 | de.flapdoodle.embed
196 | de.flapdoodle.embed.mongo.spring3x
197 | 4.16.1
198 | test
199 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/Main.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.GsonBuilder;
5 | import lombok.SneakyThrows;
6 | import lombok.extern.log4j.Log4j2;
7 | import org.springframework.boot.SpringApplication;
8 | import org.springframework.boot.autoconfigure.SpringBootApplication;
9 |
10 | import java.io.File;
11 | import java.net.http.HttpClient;
12 | import java.nio.file.Files;
13 | import java.nio.file.StandardCopyOption;
14 | import java.util.Objects;
15 |
16 | @Log4j2(topic = "Main")
17 | @SpringBootApplication
18 | public class Main {
19 | public static final Gson GSON = new GsonBuilder()
20 | .setDateFormat("MM-dd-yyyy HH:mm:ss")
21 | .create();
22 | public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
23 |
24 | @SneakyThrows
25 | public static void main(String[] args) {
26 | File config = new File("application.yml");
27 | if (!config.exists()) { // Saving the default config if it doesn't exist locally
28 | Files.copy(Objects.requireNonNull(Main.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING);
29 | log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved
30 | config.getAbsolutePath()
31 | );
32 | return;
33 | }
34 | log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
35 |
36 | SpringApplication.run(Main.class, args); // Start the application
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/AppConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.Getter;
4 | import lombok.experimental.UtilityClass;
5 |
6 | @UtilityClass
7 | public final class AppConfig {
8 | /**
9 | * Is the app running in a production environment?
10 | */
11 | @Getter
12 | private static final boolean production;
13 | static { // Are we running on production?
14 | String env = System.getenv("ENVIRONMENT");
15 | production = env != null && (env.equals("production"));
16 | }
17 |
18 | /**
19 | * Is the app running in a test environment?
20 | */
21 | @Getter
22 | private static boolean isRunningTest = true;
23 | static {
24 | try {
25 | Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
26 | } catch (ClassNotFoundException e) {
27 | isRunningTest = false;
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/CachedResponse.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 | import lombok.Setter;
7 |
8 | @AllArgsConstructor @NoArgsConstructor
9 | @Getter
10 | public class CachedResponse {
11 |
12 | /**
13 | * The cache information for this response.
14 | */
15 | private Cache cache;
16 |
17 | @AllArgsConstructor @Getter @Setter
18 | public static class Cache {
19 | /**
20 | * Whether this request is cached.
21 | */
22 | private boolean cached;
23 |
24 | /**
25 | * The unix timestamp of when this was cached.
26 | */
27 | private long cachedTime;
28 |
29 | /**
30 | * Create a new cache information object with the default values.
31 | *
32 | * The default values are:
33 | *
34 | *
35 | * - cached: true
36 | * - cachedAt: {@link System#currentTimeMillis()}
37 | *
38 | *
39 | *
40 | *
41 | * @return the default cache information object
42 | */
43 | public static Cache defaultCache() {
44 | return new Cache(true, System.currentTimeMillis());
45 | }
46 |
47 | /**
48 | * Sets if this request is cached.
49 | *
50 | * @param cached the new value of if this request is cached
51 | */
52 | public void setCached(boolean cached) {
53 | this.cached = cached;
54 | if (!cached) {
55 | cachedTime = -1;
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/ColorUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.NonNull;
4 | import lombok.experimental.UtilityClass;
5 |
6 | import java.awt.*;
7 | import java.util.HashMap;
8 | import java.util.Map;
9 | import java.util.regex.Pattern;
10 |
11 | /**
12 | * @author Braydon
13 | */
14 | @UtilityClass
15 | public final class ColorUtils {
16 | private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)§[0-9A-FK-OR]");
17 | private static final Map COLOR_MAP = new HashMap<>();
18 | static {
19 | // Map each color to its corresponding hex code
20 | COLOR_MAP.put('0', "#000000"); // Black
21 | COLOR_MAP.put('1', "#0000AA"); // Dark Blue
22 | COLOR_MAP.put('2', "#00AA00"); // Dark Green
23 | COLOR_MAP.put('3', "#00AAAA"); // Dark Aqua
24 | COLOR_MAP.put('4', "#AA0000"); // Dark Red
25 | COLOR_MAP.put('5', "#AA00AA"); // Dark Purple
26 | COLOR_MAP.put('6', "#FFAA00"); // Gold
27 | COLOR_MAP.put('7', "#AAAAAA"); // Gray
28 | COLOR_MAP.put('8', "#555555"); // Dark Gray
29 | COLOR_MAP.put('9', "#5555FF"); // Blue
30 | COLOR_MAP.put('a', "#55FF55"); // Green
31 | COLOR_MAP.put('b', "#55FFFF"); // Aqua
32 | COLOR_MAP.put('c', "#FF5555"); // Red
33 | COLOR_MAP.put('d', "#FF55FF"); // Light Purple
34 | COLOR_MAP.put('e', "#FFFF55"); // Yellow
35 | COLOR_MAP.put('f', "#FFFFFF"); // White
36 | }
37 |
38 | /**
39 | * Strip the color codes
40 | * from the given input.
41 | *
42 | * @param input the input to strip
43 | * @return the stripped input
44 | */
45 | @NonNull
46 | public static String stripColor(@NonNull String input) {
47 | return STRIP_COLOR_PATTERN.matcher(input).replaceAll("");
48 | }
49 |
50 | /**
51 | * Convert the given input
52 | * into HTML.
53 | *
54 | * @param input the input to convert
55 | * @return the HTML converted input
56 | */
57 | @NonNull
58 | public static String toHTML(@NonNull String input) {
59 | StringBuilder builder = new StringBuilder();
60 | boolean nextIsColor = false; // Is the next char a color code?
61 |
62 | // Get the leading spaces from the first line
63 | int leadingSpaces = 0;
64 | boolean foundNonSpace = false;
65 | for (char character : input.toCharArray()) {
66 | if (character == ' ' && !foundNonSpace) {
67 | leadingSpaces++;
68 | } else {
69 | foundNonSpace = true;
70 | }
71 | }
72 |
73 | for (char character : input.toCharArray()) {
74 | // Found color symbol, next color is the color
75 | if (character == '§') {
76 | nextIsColor = true;
77 | continue;
78 | }
79 | if (nextIsColor) { // Map the current color to its hex code
80 | String color = COLOR_MAP.getOrDefault(Character.toLowerCase(character), "");
81 | builder.append("");
82 | nextIsColor = false;
83 | continue;
84 | }
85 | if (character == ' ') { // Preserve space character
86 | builder.append(" ");
87 | continue;
88 | }
89 | builder.append(character); // Append the char...
90 | }
91 |
92 | // Add leading spaces to the end of the HTML string
93 | builder.append(" ".repeat(Math.max(0, leadingSpaces)));
94 |
95 | return builder.toString();
96 | }
97 |
98 | /**
99 | * Gets a {@link Color} from a Minecraft color code.
100 | *
101 | * @param colorCode the color code to get the color from
102 | * @return the color
103 | */
104 | public static Color getMinecraftColor(char colorCode) {
105 | String color = COLOR_MAP.getOrDefault(colorCode, null);
106 | if (color == null) {
107 | throw new IllegalArgumentException("Invalid color code: " + colorCode);
108 | }
109 | return Color.decode(color);
110 | }
111 |
112 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/DNSUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.NonNull;
4 | import lombok.SneakyThrows;
5 | import lombok.experimental.UtilityClass;
6 | import org.xbill.DNS.Lookup;
7 | import org.xbill.DNS.Record;
8 | import org.xbill.DNS.Type;
9 | import xyz.mcutils.backend.model.dns.impl.ARecord;
10 | import xyz.mcutils.backend.model.dns.impl.SRVRecord;
11 |
12 | /**
13 | * @author Braydon
14 | */
15 | @UtilityClass
16 | public final class DNSUtils {
17 | private static final String SRV_QUERY_PREFIX = "_minecraft._tcp.%s";
18 |
19 | /**
20 | * Get the resolved address and port of the
21 | * given hostname by resolving the SRV records.
22 | *
23 | * @param hostname the hostname to resolve
24 | * @return the resolved address and port, null if none
25 | */
26 | @SneakyThrows
27 | public static SRVRecord resolveSRV(@NonNull String hostname) {
28 | Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records
29 | if (records == null) { // No records exist
30 | return null;
31 | }
32 | SRVRecord result = null;
33 | for (Record record : records) {
34 | result = new SRVRecord((org.xbill.DNS.SRVRecord) record);
35 | }
36 | return result;
37 | }
38 |
39 | /**
40 | * Get the resolved address of the given
41 | * hostname by resolving the A records.
42 | *
43 | * @param hostname the hostname to resolve
44 | * @return the resolved address, null if none
45 | */
46 | @SneakyThrows
47 | public static ARecord resolveA(@NonNull String hostname) {
48 | Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records
49 | if (records == null) { // No records exist
50 | return null;
51 | }
52 | ARecord result = null;
53 | for (Record record : records) {
54 | result = new ARecord((org.xbill.DNS.ARecord) record);
55 | }
56 | return result;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Endpoint.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import org.springframework.http.HttpStatusCode;
6 |
7 | import java.util.List;
8 |
9 | @AllArgsConstructor @Getter
10 | public class Endpoint {
11 |
12 | /**
13 | * The endpoint.
14 | */
15 | private final String endpoint;
16 |
17 | /**
18 | * The statuses that indicate that the endpoint is online.
19 | */
20 | private final List allowedStatuses;
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/EnumUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.NonNull;
4 | import lombok.experimental.UtilityClass;
5 |
6 | /**
7 | * @author Braydon
8 | */
9 | @UtilityClass
10 | public final class EnumUtils {
11 | /**
12 | * Get the enum constant of the specified enum type with the specified name.
13 | *
14 | * @param enumType the enum type
15 | * @param name the name of the constant to return
16 | * @param the type of the enum
17 | * @return the enum constant of the specified enum type with the specified name
18 | */
19 | public > T getEnumConstant(@NonNull Class enumType, @NonNull String name) {
20 | try {
21 | return Enum.valueOf(enumType, name);
22 | } catch (IllegalArgumentException ex) {
23 | return null;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/ExpiringSet.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.NonNull;
4 | import net.jodah.expiringmap.ExpirationPolicy;
5 | import net.jodah.expiringmap.ExpiringMap;
6 |
7 | import java.util.Iterator;
8 | import java.util.Set;
9 | import java.util.concurrent.TimeUnit;
10 | import java.util.function.Consumer;
11 |
12 | /**
13 | * A simple set that expires elements after a certain
14 | * amount of time, utilizing the {@link ExpiringMap} library.
15 | *
16 | * @param The type of element to store within this set
17 | * @author Braydon
18 | */
19 | public final class ExpiringSet implements Iterable {
20 | /**
21 | * The internal cache for this set.
22 | */
23 | @NonNull private final ExpiringMap cache;
24 |
25 | /**
26 | * The lifetime (in millis) of the elements in this set.
27 | */
28 | private final long lifetime;
29 |
30 | public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit) {
31 | this(expirationPolicy, duration, timeUnit, ignored -> {});
32 | }
33 |
34 | public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit, @NonNull Consumer onExpire) {
35 | //noinspection unchecked
36 | this.cache = ExpiringMap.builder()
37 | .expirationPolicy(expirationPolicy)
38 | .expiration(duration, timeUnit)
39 | .expirationListener((key, ignored) -> onExpire.accept((T) key))
40 | .build();
41 | this.lifetime = timeUnit.toMillis(duration); // Get the lifetime in millis
42 | }
43 |
44 | /**
45 | * Add an element to this set.
46 | *
47 | * @param element the element
48 | * @return whether the element was added
49 | */
50 | public boolean add(@NonNull T element) {
51 | boolean contains = contains(element); // Does this set already contain the element?
52 | this.cache.put(element, System.currentTimeMillis() + this.lifetime);
53 | return !contains;
54 | }
55 |
56 | /**
57 | * Get the entry time of an element in this set.
58 | *
59 | * @param element the element
60 | * @return the entry time, -1 if not contained
61 | */
62 | public long getEntryTime(@NonNull T element) {
63 | return contains(element) ? this.cache.get(element) - this.lifetime : -1L;
64 | }
65 |
66 | /**
67 | * Check if an element is
68 | * contained within this set.
69 | *
70 | * @param element the element
71 | * @return whether the element is contained
72 | */
73 | public boolean contains(@NonNull T element) {
74 | Long timeout = this.cache.get(element); // Get the timeout for the element
75 | return timeout != null && (timeout > System.currentTimeMillis());
76 | }
77 |
78 | /**
79 | * Check if this set is empty.
80 | *
81 | * @return whether this set is empty
82 | */
83 | public boolean isEmpty() {
84 | return this.cache.isEmpty();
85 | }
86 |
87 | /**
88 | * Get the size of this set.
89 | *
90 | * @return the size
91 | */
92 | public int size() {
93 | return this.cache.size();
94 | }
95 |
96 | /**
97 | * Remove an element from this set.
98 | *
99 | * @param element the element
100 | * @return whether the element was removed
101 | */
102 | public boolean remove(@NonNull T element) {
103 | return this.cache.remove(element) != null;
104 | }
105 |
106 | /**
107 | * Clear this set.
108 | */
109 | public void clear() {
110 | this.cache.clear();
111 | }
112 |
113 | /**
114 | * Get the elements in this set.
115 | *
116 | * @return the elements
117 | */
118 | @NonNull
119 | public Set getElements() {
120 | return this.cache.keySet();
121 | }
122 |
123 | /**
124 | * Returns an iterator over elements of type {@code T}.
125 | *
126 | * @return an Iterator.
127 | */
128 | @Override @NonNull
129 | public Iterator iterator() {
130 | return this.cache.keySet().iterator();
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Fonts.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.extern.log4j.Log4j2;
4 | import xyz.mcutils.backend.Main;
5 |
6 | import java.awt.*;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 |
10 | @Log4j2(topic = "Fonts")
11 | public class Fonts {
12 |
13 | public static final Font MINECRAFT;
14 | public static final Font MINECRAFT_BOLD;
15 | public static final Font MINECRAFT_ITALIC;
16 |
17 | static {
18 | InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
19 | try {
20 | MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
21 | MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
22 | MINECRAFT_ITALIC = MINECRAFT.deriveFont(Font.ITALIC);
23 | } catch (FontFormatException | IOException e) {
24 | log.error("Failed to load Minecraft font", e);
25 | throw new RuntimeException("Failed to load Minecraft font", e);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/IPUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import lombok.experimental.UtilityClass;
5 |
6 | @UtilityClass
7 | public class IPUtils {
8 | /**
9 | * The headers that contain the IP.
10 | */
11 | private static final String[] IP_HEADERS = new String[] {
12 | "CF-Connecting-IP",
13 | "X-Forwarded-For"
14 | };
15 |
16 | /**
17 | * Get the real IP from the given request.
18 | *
19 | * @param request the request
20 | * @return the real IP
21 | */
22 | public static String getRealIp(HttpServletRequest request) {
23 | String ip = request.getRemoteAddr();
24 | for (String headerName : IP_HEADERS) {
25 | String header = request.getHeader(headerName);
26 | if (header == null) {
27 | continue;
28 | }
29 | if (!header.contains(",")) { // Handle single IP
30 | ip = header;
31 | break;
32 | }
33 | // Handle multiple IPs
34 | String[] ips = header.split(",");
35 | for (String ipHeader : ips) {
36 | ip = ipHeader;
37 | break;
38 | }
39 | }
40 | return ip;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/ImageUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import jakarta.validation.constraints.NotNull;
4 | import lombok.SneakyThrows;
5 | import lombok.extern.log4j.Log4j2;
6 |
7 | import javax.imageio.ImageIO;
8 | import java.awt.*;
9 | import java.awt.geom.AffineTransform;
10 | import java.awt.image.BufferedImage;
11 | import java.io.ByteArrayInputStream;
12 | import java.io.ByteArrayOutputStream;
13 | import java.util.Base64;
14 |
15 | @Log4j2(topic = "Image Utils")
16 | public class ImageUtils {
17 | /**
18 | * Scale the given image to the provided scale.
19 | *
20 | * @param image the image to scale
21 | * @param scale the scale to scale the image to
22 | * @return the scaled image
23 | */
24 | public static BufferedImage resize(BufferedImage image, double scale) {
25 | BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
26 | Graphics2D graphics = scaled.createGraphics();
27 | graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
28 | graphics.dispose();
29 | return scaled;
30 | }
31 |
32 | /**
33 | * Flip the given image.
34 | *
35 | * @param image the image to flip
36 | * @return the flipped image
37 | */
38 | public static BufferedImage flip(@NotNull final BufferedImage image) {
39 | BufferedImage flipped = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
40 | Graphics2D graphics = flipped.createGraphics();
41 | graphics.drawImage(image, image.getWidth(), 0, 0, image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), null);
42 | graphics.dispose();
43 | return flipped;
44 | }
45 |
46 | /**
47 | * Convert an image to bytes.
48 | *
49 | * @param image the image to convert
50 | * @return the image as bytes
51 | */
52 | @SneakyThrows
53 | public static byte[] imageToBytes(BufferedImage image) {
54 | try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
55 | ImageIO.write(image, "png", outputStream);
56 | return outputStream.toByteArray();
57 | } catch (Exception e) {
58 | throw new Exception("Failed to convert image to bytes", e);
59 | }
60 | }
61 |
62 | /**
63 | * Convert a base64 string to an image.
64 | *
65 | * @param base64 the base64 string to convert
66 | * @return the image
67 | */
68 | @SneakyThrows
69 | public static BufferedImage base64ToImage(String base64) {
70 | String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
71 |
72 | try {
73 | return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
74 | } catch (Exception e) {
75 | throw new Exception("Failed to convert base64 to image", e);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/JavaMinecraftVersion.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.Getter;
4 | import lombok.NonNull;
5 | import lombok.RequiredArgsConstructor;
6 | import lombok.ToString;
7 | import lombok.extern.log4j.Log4j2;
8 |
9 | /**
10 | * @author Braydon
11 | * @see Protocol Version Numbers
12 | * @see Spigot NMS (1.16+)
13 | * @see Spigot NMS (1.10 - 1.15)
14 | * @see Spigot NMS (1.8 - 1.9)
15 | */
16 | @RequiredArgsConstructor @Getter @ToString @Log4j2(topic = "Minecraft Version")
17 | public enum JavaMinecraftVersion {
18 | V1_20_3(765, "v1_20_R3"), // 1.20.3 & 1.20.4
19 | V1_20_2(764, "v1_20_R2"), // 1.20.2
20 | V1_20(763, "v1_20_R1"), // 1.20 & 1.20.1
21 |
22 | V1_19_4(762, "v1_19_R3"), // 1.19.4
23 | V1_19_3(761, "v1_19_R2"), // 1.19.3
24 | V1_19_1(760, "v1_19_R1"), // 1.19.1 & 1.19.2
25 | V1_19(759, "v1_19_R1"), // 1.19
26 |
27 | V1_18_2(758, "v1_18_R2"), // 1.18.2
28 | V1_18(757, "v1_18_R1"), // 1.18 & 1.18.1
29 |
30 | V1_17_1(756, "v1_17_R1"), // 1.17.1
31 | V1_17(755, "v1_17_R1"), // 1.17
32 |
33 | V1_16_4(754, "v1_16_R3"), // 1.16.4 & 1.16.5
34 | V1_16_3(753, "v1_16_R2"), // 1.16.3
35 | V1_16_2(751, "v1_16_R2"), // 1.16.2
36 | V1_16_1(736, "v1_16_R1"), // 1.16.1
37 | V1_16(735, "v1_16_R1"), // 1.16
38 |
39 | V1_15_2(578, "v1_15_R1"), // 1.15.2
40 | V1_15_1(575, "v1_15_R1"), // 1.15.1
41 | V1_15(573, "v1_15_R1"), // 1.15
42 |
43 | V1_14_4(498, "v1_14_R1"), // 1.14.4
44 | V1_14_3(490, "v1_14_R1"), // 1.14.3
45 | V1_14_2(485, "v1_14_R1"), // 1.14.2
46 | V1_14_1(480, "v1_14_R1"), // 1.14.1
47 | V1_14(477, "v1_14_R1"), // 1.14
48 |
49 | V1_13_2(404, "v1_13_R2"), // 1.13.2
50 | V1_13_1(401, "v1_13_R2"), // 1.13.1
51 | V1_13(393, "v1_13_R1"), // 1.13
52 |
53 | V1_12_2(340, "v1_12_R1"), // 1.12.2
54 | V1_12_1(338, "v1_12_R1"), // 1.12.1
55 | V1_12(335, "v1_12_R1"), // 1.12
56 |
57 | V1_11_1(316, "v1_11_R1"), // 1.11.1 & 1.11.2
58 | V1_11(315, "v1_11_R1"), // 1.11
59 |
60 | V1_10(210, "v1_10_R1"), // 1.10.x
61 |
62 | V1_9_3(110, "v1_9_R2"), // 1.9.3 & 1.9.4
63 | V1_9_2(109, "v1_9_R1"), // 1.9.2
64 | V1_9_1(108, "v1_9_R1"), // 1.9.1
65 | V1_9(107, "v1_9_R1"), // 1.9
66 |
67 | V1_8(47, "v1_8_R3"), // 1.8.x
68 |
69 | V1_7_6(5, "v1_7_R4"), // 1.7.6 - 1.7.10
70 |
71 | UNKNOWN(-1, "Unknown");
72 |
73 | // Game Updates
74 | public static final JavaMinecraftVersion TRAILS_AND_TALES = JavaMinecraftVersion.V1_20;
75 | public static final JavaMinecraftVersion THE_WILD_UPDATE = JavaMinecraftVersion.V1_19;
76 | public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_2 = JavaMinecraftVersion.V1_18;
77 | public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_1 = JavaMinecraftVersion.V1_17;
78 | public static final JavaMinecraftVersion NETHER_UPDATE = JavaMinecraftVersion.V1_16;
79 | public static final JavaMinecraftVersion BUZZY_BEES = JavaMinecraftVersion.V1_15;
80 | public static final JavaMinecraftVersion VILLAGE_AND_PILLAGE = JavaMinecraftVersion.V1_14;
81 | public static final JavaMinecraftVersion UPDATE_AQUATIC = JavaMinecraftVersion.V1_13;
82 | public static final JavaMinecraftVersion WORLD_OF_COLOR_UPDATE = JavaMinecraftVersion.V1_12;
83 | public static final JavaMinecraftVersion EXPLORATION_UPDATE = JavaMinecraftVersion.V1_11;
84 | public static final JavaMinecraftVersion FROSTBURN_UPDATE = JavaMinecraftVersion.V1_10;
85 | public static final JavaMinecraftVersion THE_COMBAT_UPDATE = JavaMinecraftVersion.V1_9;
86 | public static final JavaMinecraftVersion BOUNTIFUL_UPDATE = JavaMinecraftVersion.V1_8;
87 |
88 | private static final JavaMinecraftVersion[] VALUES = JavaMinecraftVersion.values();
89 |
90 | /**
91 | * The protocol number of this version.
92 | */
93 | private final int protocol;
94 |
95 | /**
96 | * The server version for this version.
97 | */
98 | private final String nmsVersion;
99 |
100 | /**
101 | * The cached name of this version.
102 | */
103 | private String name;
104 |
105 | /**
106 | * Get the name of this protocol version.
107 | *
108 | * @return the name
109 | */
110 | public String getName() {
111 | // We have a name
112 | if (this.name != null) {
113 | return this.name;
114 | }
115 | // Use the server version as the name if unknown
116 | if (this == UNKNOWN) {
117 | this.name = this.getNmsVersion();
118 | } else { // Parse the name
119 | this.name = name().substring(1);
120 | this.name = this.name.replace("_", ".");
121 | }
122 | return this.name;
123 | }
124 |
125 | /**
126 | * Is this version legacy?
127 | *
128 | * @return whether this version is legacy
129 | */
130 | public boolean isLegacy() {
131 | return this.isBelow(JavaMinecraftVersion.V1_16);
132 | }
133 |
134 | /**
135 | * Check if this version is
136 | * above the one given.
137 | *
138 | * @param other the other version
139 | * @return true if above, otherwise false
140 | */
141 | public boolean isAbove(JavaMinecraftVersion other) {
142 | return this.protocol > other.getProtocol();
143 | }
144 |
145 | /**
146 | * Check if this version is
147 | * or above the one given.
148 | *
149 | * @param other the other version
150 | * @return true if is or above, otherwise false
151 | */
152 | public boolean isOrAbove(JavaMinecraftVersion other) {
153 | return this.protocol >= other.getProtocol();
154 | }
155 |
156 | /**
157 | * Check if this version is
158 | * below the one given.
159 | *
160 | * @param other the other version
161 | * @return true if below, otherwise false
162 | */
163 | public boolean isBelow(JavaMinecraftVersion other) {
164 | return this.protocol < other.getProtocol();
165 | }
166 |
167 | /**
168 | * Check if this version is
169 | * or below the one given.
170 | *
171 | * @param other the other version
172 | * @return true if is or below, otherwise false
173 | */
174 | public boolean isOrBelow(JavaMinecraftVersion other) {
175 | return this.protocol <= other.getProtocol();
176 | }
177 |
178 | /**
179 | * Get the minimum Minecraft version.
180 | *
181 | * @return the minimum version
182 | */
183 | @NonNull
184 | public static JavaMinecraftVersion getMinimumVersion() {
185 | return VALUES[VALUES.length - 2];
186 | }
187 |
188 | /**
189 | * Get the version from the given protocol.
190 | *
191 | * @param protocol the protocol to get the version for
192 | * @return the version, null if none
193 | */
194 | public static JavaMinecraftVersion byProtocol(int protocol) {
195 | for (JavaMinecraftVersion version : values()) {
196 | if (version.getProtocol() == protocol) {
197 | return version;
198 | }
199 | }
200 | return null;
201 | }
202 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/MojangServer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NonNull;
6 | import lombok.ToString;
7 |
8 | import java.io.IOException;
9 | import java.net.InetAddress;
10 | import java.net.UnknownHostException;
11 | import java.util.concurrent.TimeUnit;
12 |
13 | /**
14 | * @author Fascinated (fascinated7)
15 | */
16 | @AllArgsConstructor
17 | @Getter
18 | @ToString
19 | public enum MojangServer {
20 | SESSION("Session Server", "https://sessionserver.mojang.com"),
21 | API("Mojang API", "https://api.mojang.com"),
22 | TEXTURES("Textures Server", "https://textures.minecraft.net"),
23 | ASSETS("Assets Server", "https://assets.mojang.com"),
24 | LIBRARIES("Libraries Server", "https://libraries.minecraft.net"),
25 | SERVICES("Minecraft Services", "https://api.minecraftservices.com");
26 |
27 | private static final long STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
28 |
29 | /**
30 | * The name of this server.
31 | */
32 | @NonNull private final String name;
33 |
34 | /**
35 | * The endpoint of this service.
36 | */
37 | @NonNull private final String endpoint;
38 |
39 | /**
40 | * Ping this service and get the status of it.
41 | *
42 | * @return the service status
43 | */
44 | @NonNull
45 | public Status getStatus() {
46 | try {
47 | InetAddress address = InetAddress.getByName(endpoint.substring(8));
48 | long before = System.currentTimeMillis();
49 | if (address.isReachable((int) STATUS_TIMEOUT)) {
50 | // The time it took to reach the host is 75% of
51 | // the timeout, consider it to be degraded.
52 | if ((System.currentTimeMillis() - before) > STATUS_TIMEOUT * 0.75D) {
53 | return Status.DEGRADED;
54 | }
55 | return Status.ONLINE;
56 | }
57 | } catch (UnknownHostException ex) {
58 | ex.printStackTrace();
59 | } catch (IOException ignored) {
60 | // We can safely ignore any errors, we're simply checking
61 | // if the host is reachable, if it's not, then it's offline.
62 | }
63 | return Status.OFFLINE;
64 | }
65 |
66 | /**
67 | * The status of a service.
68 | */
69 | public enum Status {
70 | /**
71 | * The service is online and accessible.
72 | */
73 | ONLINE,
74 |
75 | /**
76 | * The service is online, but is experiencing degraded performance.
77 | */
78 | DEGRADED,
79 |
80 | /**
81 | * The service is offline and inaccessible.
82 | */
83 | OFFLINE
84 | }
85 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/PlayerUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import lombok.SneakyThrows;
5 | import lombok.experimental.UtilityClass;
6 | import lombok.extern.log4j.Log4j2;
7 | import xyz.mcutils.backend.Main;
8 | import xyz.mcutils.backend.exception.impl.BadRequestException;
9 |
10 | import java.net.URI;
11 | import java.net.http.HttpRequest;
12 | import java.net.http.HttpResponse;
13 | import java.util.UUID;
14 |
15 | @UtilityClass @Log4j2(topic = "Player Utils")
16 | public class PlayerUtils {
17 |
18 | /**
19 | * Gets the UUID from the string.
20 | *
21 | * @param id the id string
22 | * @return the UUID
23 | */
24 | public static UUID getUuidFromString(String id) {
25 | UUID uuid;
26 | boolean isFullUuid = id.length() == 36;
27 | if (id.length() == 32 || isFullUuid) {
28 | try {
29 | uuid = isFullUuid ? UUID.fromString(id) : UUIDUtils.addDashes(id);
30 | } catch (IllegalArgumentException exception) {
31 | throw new BadRequestException("Invalid UUID provided: %s".formatted(id));
32 | }
33 | return uuid;
34 | }
35 | return null;
36 | }
37 |
38 | /**
39 | * Gets the skin data from the URL.
40 | *
41 | * @return the skin data
42 | */
43 | @SneakyThrows
44 | @JsonIgnore
45 | public static byte[] getSkinImage(String url) {
46 | HttpResponse response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
47 | HttpResponse.BodyHandlers.ofByteArray());
48 | return response.body();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/ServerUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.experimental.UtilityClass;
4 |
5 | @UtilityClass
6 | public class ServerUtils {
7 |
8 | /**
9 | * Gets the address of the server.
10 | *
11 | * @return the address of the server
12 | */
13 | public static String getAddress(String ip, int port) {
14 | return ip + (port == 25565 ? "" : ":" + port);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Timer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | public class Timer {
4 |
5 | /**
6 | * Schedules a task to run after a delay.
7 | *
8 | * @param runnable the task to run
9 | * @param delay the delay before the task runs
10 | */
11 | public static void scheduleRepeating(Runnable runnable, long delay, long period) {
12 | new java.util.Timer().scheduleAtFixedRate(new java.util.TimerTask() {
13 | @Override
14 | public void run() {
15 | runnable.run();
16 | }
17 | }, delay, period);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Tuple.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @Getter @AllArgsConstructor
7 | public class Tuple {
8 |
9 | /**
10 | * The left value of the tuple.
11 | */
12 | private final L left;
13 |
14 | /**
15 | * The right value of the tuple.
16 | */
17 | private final R right;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/UUIDUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import lombok.experimental.UtilityClass;
5 |
6 | import java.util.UUID;
7 |
8 | @UtilityClass
9 | public class UUIDUtils {
10 |
11 | /**
12 | * Add dashes to a UUID.
13 | *
14 | * @param trimmed the UUID without dashes
15 | * @return the UUID with dashes
16 | */
17 | @NonNull
18 | public static UUID addDashes(@NonNull String trimmed) {
19 | StringBuilder builder = new StringBuilder(trimmed);
20 | for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
21 | builder.insert(pos, "-");
22 | }
23 | return UUID.fromString(builder.toString());
24 | }
25 |
26 | /**
27 | * Remove dashes from a UUID.
28 | *
29 | * @param dashed the UUID with dashes
30 | * @return the UUID without dashes
31 | */
32 | @NonNull
33 | public static String removeDashes(@NonNull UUID dashed) {
34 | return dashed.toString().replace("-", "");
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/WebRequest.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.experimental.UtilityClass;
4 | import org.springframework.http.HttpStatus;
5 | import org.springframework.http.HttpStatusCode;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
8 | import org.springframework.web.client.RestClient;
9 | import xyz.mcutils.backend.exception.impl.RateLimitException;
10 |
11 | @UtilityClass
12 | public class WebRequest {
13 |
14 | /**
15 | * The web client.
16 | */
17 | private static final RestClient CLIENT;
18 |
19 | static {
20 | HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
21 | requestFactory.setConnectTimeout(2500); // 2.5 seconds
22 | CLIENT = RestClient.builder()
23 | .requestFactory(requestFactory)
24 | .build();
25 | }
26 |
27 | /**
28 | * Gets a response from the given URL.
29 | *
30 | * @param url the url
31 | * @return the response
32 | * @param the type of the response
33 | */
34 | public static T getAsEntity(String url, Class clazz) throws RateLimitException {
35 | ResponseEntity responseEntity = CLIENT.get()
36 | .uri(url)
37 | .retrieve()
38 | .onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
39 | .toEntity(clazz);
40 |
41 | if (responseEntity.getStatusCode().isError()) {
42 | return null;
43 | }
44 | if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
45 | throw new RateLimitException("Rate limit reached");
46 | }
47 | return responseEntity.getBody();
48 | }
49 |
50 | /**
51 | * Gets a response from the given URL.
52 | *
53 | * @param url the url
54 | * @return the response
55 | */
56 | public static ResponseEntity> get(String url, Class> clazz) {
57 | return CLIENT.get()
58 | .uri(url)
59 | .retrieve()
60 | .onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
61 | .toEntity(clazz);
62 | }
63 |
64 | /**
65 | * Gets a response from the given URL.
66 | *
67 | * @param url the url
68 | * @return the response
69 | */
70 | public static ResponseEntity> head(String url, Class> clazz) {
71 | return CLIENT.head()
72 | .uri(url)
73 | .retrieve()
74 | .onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
75 | .toEntity(clazz);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/MinecraftBedrockPacket.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet;
2 |
3 | import lombok.NonNull;
4 |
5 | import java.io.IOException;
6 | import java.net.DatagramSocket;
7 |
8 | /**
9 | * Represents a packet in the
10 | * Minecraft Bedrock protocol.
11 | *
12 | * @author Braydon
13 | * @see Protocol Docs
14 | */
15 | public interface MinecraftBedrockPacket {
16 | /**
17 | * Process this packet.
18 | *
19 | * @param socket the socket to process the packet for
20 | * @throws IOException if an I/O error occurs
21 | */
22 | void process(@NonNull DatagramSocket socket) throws IOException;
23 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/MinecraftJavaPacket.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet;
2 |
3 | import lombok.NonNull;
4 |
5 | import java.io.DataInputStream;
6 | import java.io.DataOutputStream;
7 | import java.io.IOException;
8 |
9 | /**
10 | * Represents a packet in the
11 | * Minecraft Java protocol.
12 | *
13 | * @author Braydon
14 | * @see Protocol Docs
15 | */
16 | public abstract class MinecraftJavaPacket {
17 | /**
18 | * Process this packet.
19 | *
20 | * @param inputStream the input stream to read from
21 | * @param outputStream the output stream to write to
22 | * @throws IOException if an I/O error occurs
23 | */
24 | public abstract void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException;
25 |
26 | /**
27 | * Write a variable integer to the output stream.
28 | *
29 | * @param outputStream the output stream to write to
30 | * @param paramInt the integer to write
31 | * @throws IOException if an I/O error occurs
32 | */
33 | protected final void writeVarInt(DataOutputStream outputStream, int paramInt) throws IOException {
34 | while (true) {
35 | if ((paramInt & 0xFFFFFF80) == 0) {
36 | outputStream.writeByte(paramInt);
37 | return;
38 | }
39 | outputStream.writeByte(paramInt & 0x7F | 0x80);
40 | paramInt >>>= 7;
41 | }
42 | }
43 |
44 | /**
45 | * Read a variable integer from the input stream.
46 | *
47 | * @param inputStream the input stream to read from
48 | * @return the integer that was read
49 | * @throws IOException if an I/O error occurs
50 | */
51 | protected final int readVarInt(@NonNull DataInputStream inputStream) throws IOException {
52 | int i = 0;
53 | int j = 0;
54 | while (true) {
55 | int k = inputStream.readByte();
56 | i |= (k & 0x7F) << j++ * 7;
57 | if (j > 5) {
58 | throw new RuntimeException("VarInt too big");
59 | }
60 | if ((k & 0x80) != 128) {
61 | break;
62 | }
63 | }
64 | return i;
65 | }
66 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet.impl.bedrock;
2 |
3 | import lombok.NonNull;
4 | import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
5 |
6 | import java.io.IOException;
7 | import java.net.DatagramPacket;
8 | import java.net.DatagramSocket;
9 | import java.nio.ByteBuffer;
10 | import java.nio.ByteOrder;
11 |
12 | /**
13 | * This packet is sent by the client to the server to
14 | * request a pong response from the server. The server
15 | * will respond with a string containing the server's status.
16 | *
17 | * @author Braydon
18 | * @see Protocol Docs
19 | */
20 | public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
21 | private static final byte ID = 0x01; // The ID of the packet
22 | private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };
23 |
24 | /**
25 | * Process this packet.
26 | *
27 | * @param socket the socket to process the packet for
28 | * @throws IOException if an I/O error occurs
29 | */
30 | @Override
31 | public void process(@NonNull DatagramSocket socket) throws IOException {
32 | // Construct the packet buffer
33 | ByteBuffer buffer = ByteBuffer.allocate(33).order(ByteOrder.LITTLE_ENDIAN);;
34 | buffer.put(ID); // Packet ID
35 | buffer.putLong(System.currentTimeMillis()); // Timestamp
36 | buffer.put(MAGIC); // Magic
37 | buffer.putLong(0L); // Client GUID
38 |
39 | // Send the packet
40 | socket.send(new DatagramPacket(buffer.array(), 0, buffer.limit()));
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet.impl.bedrock;
2 |
3 | import lombok.Getter;
4 | import lombok.NonNull;
5 | import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
6 | import xyz.mcutils.backend.model.server.BedrockMinecraftServer;
7 |
8 | import java.io.IOException;
9 | import java.net.DatagramPacket;
10 | import java.net.DatagramSocket;
11 | import java.nio.ByteBuffer;
12 | import java.nio.ByteOrder;
13 | import java.nio.charset.StandardCharsets;
14 |
15 | /**
16 | * This packet is sent by the server to the client in
17 | * response to the {@link BedrockPacketUnconnectedPing}.
18 | *
19 | * @author Braydon
20 | * @see Protocol Docs
21 | */
22 | @Getter
23 | public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
24 | private static final byte ID = 0x1C; // The ID of the packet
25 |
26 | /**
27 | * The response from the server, null if none.
28 | */
29 | private String response;
30 |
31 | /**
32 | * Process this packet.
33 | *
34 | * @param socket the socket to process the packet for
35 | * @throws IOException if an I/O error occurs
36 | */
37 | @Override
38 | public void process(@NonNull DatagramSocket socket) throws IOException {
39 | // Handle receiving of the packet
40 | byte[] receiveData = new byte[2048];
41 | DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
42 | socket.receive(receivePacket);
43 |
44 | // Construct a buffer from the received packet
45 | ByteBuffer buffer = ByteBuffer.wrap(receivePacket.getData()).order(ByteOrder.LITTLE_ENDIAN);
46 | byte id = buffer.get(); // The received packet id
47 | if (id == ID) {
48 | String response = new String(buffer.array(), StandardCharsets.UTF_8).trim(); // Extract the response
49 |
50 | // Trim the length of the response (short) from the
51 | // start of the string, which begins with the edition name
52 | for (BedrockMinecraftServer.Edition edition : BedrockMinecraftServer.Edition.values()) {
53 | int startIndex = response.indexOf(edition.name());
54 | if (startIndex != -1) {
55 | response = response.substring(startIndex);
56 | break;
57 | }
58 | }
59 | this.response = response;
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet.impl.java;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.NonNull;
5 | import lombok.ToString;
6 | import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
7 |
8 | import java.io.ByteArrayOutputStream;
9 | import java.io.DataInputStream;
10 | import java.io.DataOutputStream;
11 | import java.io.IOException;
12 |
13 | /**
14 | * This packet is sent by the client to the server to set
15 | * the hostname, port, and protocol version of the client.
16 | *
17 | * @author Braydon
18 | * @see Protocol Docs
19 | */
20 | @AllArgsConstructor @ToString
21 | public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket {
22 | private static final byte ID = 0x00; // The ID of the packet
23 | private static final int STATUS_HANDSHAKE = 1; // The status handshake ID
24 |
25 | /**
26 | * The hostname of the server.
27 | */
28 | @NonNull private final String hostname;
29 |
30 | /**
31 | * The port of the server.
32 | */
33 | private final int port;
34 |
35 | /**
36 | * The protocol version of the server.
37 | */
38 | private final int protocolVersion;
39 |
40 | /**
41 | * Process this packet.
42 | *
43 | * @param inputStream the input stream to read from
44 | * @param outputStream the output stream to write to
45 | * @throws IOException if an I/O error occurs
46 | */
47 | @Override
48 | public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
49 | try (ByteArrayOutputStream handshakeBytes = new ByteArrayOutputStream();
50 | DataOutputStream handshake = new DataOutputStream(handshakeBytes)
51 | ) {
52 | handshake.writeByte(ID); // Write the ID of the packet
53 | writeVarInt(handshake, protocolVersion); // Write the protocol version
54 | writeVarInt(handshake, hostname.length()); // Write the length of the hostname
55 | handshake.writeBytes(hostname); // Write the hostname
56 | handshake.writeShort(port); // Write the port
57 | writeVarInt(handshake, STATUS_HANDSHAKE); // Write the status handshake ID
58 |
59 | // Write the handshake bytes to the output stream
60 | writeVarInt(outputStream, handshakeBytes.size());
61 | outputStream.write(handshakeBytes.toByteArray());
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/impl/java/JavaPacketStatusInStart.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet.impl.java;
2 |
3 | import lombok.Getter;
4 | import lombok.NonNull;
5 | import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
6 |
7 | import java.io.DataInputStream;
8 | import java.io.DataOutputStream;
9 | import java.io.IOException;
10 |
11 | /**
12 | * This packet is sent by the client to the server to request the
13 | * status of the server. The server will respond with a json object
14 | * containing the server's status.
15 | *
16 | * @author Braydon
17 | * @see Protocol Docs
18 | */
19 | @Getter
20 | public final class JavaPacketStatusInStart extends MinecraftJavaPacket {
21 | private static final byte ID = 0x00; // The ID of the packet
22 |
23 | /**
24 | * The response json from the server, null if none.
25 | */
26 | private String response;
27 |
28 | /**
29 | * Process this packet.
30 | *
31 | * @param inputStream the input stream to read from
32 | * @param outputStream the output stream to write to
33 | * @throws IOException if an I/O error occurs
34 | */
35 | @Override
36 | public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException {
37 | // Send the status request
38 | outputStream.writeByte(0x01); // Size of packet
39 | outputStream.writeByte(ID);
40 |
41 | // Read the status response
42 | readVarInt(inputStream); // Size of the response
43 | int id = readVarInt(inputStream);
44 | if (id == -1) { // The stream was prematurely ended
45 | throw new IOException("Server prematurely ended stream.");
46 | } else if (id != ID) { // Invalid packet ID
47 | throw new IOException("Server returned invalid packet ID.");
48 | }
49 |
50 | int length = readVarInt(inputStream); // Length of the response
51 | if (length == -1) { // The stream was prematurely ended
52 | throw new IOException("Server prematurely ended stream.");
53 | } else if (length == 0) {
54 | throw new IOException("Server returned unexpected value.");
55 | }
56 |
57 | // Get the json response
58 | byte[] data = new byte[length];
59 | inputStream.readFully(data);
60 | response = new String(data);
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/IsometricSkinRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer;
2 |
3 | import xyz.mcutils.backend.model.skin.ISkinPart;
4 |
5 | import java.awt.*;
6 | import java.awt.geom.AffineTransform;
7 | import java.awt.image.BufferedImage;
8 |
9 | public abstract class IsometricSkinRenderer extends SkinRenderer {
10 |
11 | /**
12 | * Draw a part onto the texture.
13 | *
14 | * @param graphics the graphics to draw to
15 | * @param partImage the part image to draw
16 | * @param transform the transform to apply
17 | * @param x the x position to draw at
18 | * @param y the y position to draw at
19 | * @param width the part image width
20 | * @param height the part image height
21 | */
22 | protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform,
23 | double x, double y, int width, int height) {
24 | graphics.setTransform(transform);
25 | graphics.drawImage(partImage, (int) x, (int) y, width, height, null);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer;
2 |
3 | import java.awt.image.BufferedImage;
4 |
5 | public abstract class Renderer {
6 |
7 | /**
8 | * Renders the object to the specified size.
9 | *
10 | * @param input The object to render.
11 | * @param size The size to render the object to.
12 | */
13 | public abstract BufferedImage render(T input, int size);
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/SkinRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer;
2 |
3 | import lombok.SneakyThrows;
4 | import lombok.extern.log4j.Log4j2;
5 | import xyz.mcutils.backend.common.ImageUtils;
6 | import xyz.mcutils.backend.model.skin.ISkinPart;
7 | import xyz.mcutils.backend.model.skin.Skin;
8 |
9 | import javax.imageio.ImageIO;
10 | import java.awt.*;
11 | import java.awt.image.BufferedImage;
12 | import java.io.ByteArrayInputStream;
13 |
14 | @Log4j2(topic = "Skin Renderer")
15 | public abstract class SkinRenderer {
16 |
17 | /**
18 | * Get the texture of a part of the skin.
19 | *
20 | * @param skin the skin to get the part texture from
21 | * @param part the part of the skin to get
22 | * @param size the size to scale the texture to
23 | * @param renderOverlays should the overlays be rendered
24 | * @return the texture of the skin part
25 | */
26 | @SneakyThrows
27 | public BufferedImage getVanillaSkinPart(Skin skin, ISkinPart.Vanilla part, double size, boolean renderOverlays) {
28 | ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part
29 |
30 | // The skin texture is legacy, use legacy coordinates
31 | if (skin.isLegacy() && part.hasLegacyCoordinates()) {
32 | coordinates = part.getLegacyCoordinates();
33 | }
34 | int width = part.getWidth(); // The width of the part
35 | if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) {
36 | width--;
37 | }
38 | BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture
39 | BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size);
40 | if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) {
41 | partTexture = ImageUtils.flip(partTexture);
42 | }
43 |
44 | // Draw part overlays
45 | ISkinPart.Vanilla[] overlayParts = part.getOverlays();
46 | if (overlayParts != null && renderOverlays) {
47 | log.info("Applying overlays to part: {}", part.name());
48 | for (ISkinPart.Vanilla overlay : overlayParts) {
49 | applyOverlay(partTexture.createGraphics(), getVanillaSkinPart(skin, overlay, size, false));
50 | }
51 | }
52 |
53 | return partTexture;
54 | }
55 |
56 | /**
57 | * Get the texture of a specific part of the skin.
58 | *
59 | * @param skinImage the skin image to get the part from
60 | * @param x the x position of the part
61 | * @param y the y position of the part
62 | * @param width the width of the part
63 | * @param height the height of the part
64 | * @param size the size to scale the part to
65 | * @return the texture of the skin part
66 | */
67 | @SneakyThrows
68 | private BufferedImage getSkinPartTexture(BufferedImage skinImage, int x, int y, int width, int height, double size) {
69 | // Create a new BufferedImage for the part of the skin texture
70 | BufferedImage headTexture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
71 |
72 | // Crop just the part we want based on our x, y, width, and height
73 | headTexture.getGraphics().drawImage(skinImage, 0, 0, width, height, x, y, x + width, y + height, null);
74 |
75 | // Scale the skin part texture
76 | if (size > 0D) {
77 | headTexture = ImageUtils.resize(headTexture, size);
78 | }
79 | return headTexture;
80 | }
81 |
82 | /**
83 | * Apply an overlay to a texture.
84 | *
85 | * @param graphics the graphics to overlay on
86 | * @param overlayImage the part to overlay
87 | */
88 | protected void applyOverlay(Graphics2D graphics, BufferedImage overlayImage) {
89 | try {
90 | graphics.drawImage(overlayImage, 0, 0, null);
91 | graphics.dispose();
92 | } catch (Exception ignored) {
93 | // We can safely ignore this, legacy
94 | // skins don't have overlays
95 | }
96 | }
97 |
98 | /**
99 | * Renders the skin part for the player's skin.
100 | *
101 | * @param skin the player's skin
102 | * @param part the skin part to render
103 | * @param renderOverlays should the overlays be rendered
104 | * @param size the size of the part
105 | * @return the rendered skin part
106 | */
107 | public abstract BufferedImage render(Skin skin, T part, boolean renderOverlays, int size);
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/impl/server/ServerPreviewRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer.impl.server;
2 |
3 | import lombok.extern.log4j.Log4j2;
4 | import xyz.mcutils.backend.Main;
5 | import xyz.mcutils.backend.common.ColorUtils;
6 | import xyz.mcutils.backend.common.Fonts;
7 | import xyz.mcutils.backend.common.ImageUtils;
8 | import xyz.mcutils.backend.common.renderer.Renderer;
9 | import xyz.mcutils.backend.model.server.JavaMinecraftServer;
10 | import xyz.mcutils.backend.model.server.MinecraftServer;
11 | import xyz.mcutils.backend.service.ServerService;
12 |
13 | import javax.imageio.ImageIO;
14 | import java.awt.*;
15 | import java.awt.image.BufferedImage;
16 | import java.io.ByteArrayInputStream;
17 |
18 | @Log4j2
19 | public class ServerPreviewRenderer extends Renderer {
20 | public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
21 |
22 |
23 | private static BufferedImage SERVER_BACKGROUND;
24 | private static BufferedImage PING_ICON;
25 | static {
26 | try {
27 | SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
28 | PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
29 | } catch (Exception ex) {
30 | log.error("Failed to load server preview assets", ex);
31 | }
32 | }
33 |
34 | private final int fontSize = Fonts.MINECRAFT.getSize();
35 | private final int width = 560;
36 | private final int height = 64 + 3 + 3;
37 | private final int padding = 3;
38 |
39 | @Override
40 | public BufferedImage render(MinecraftServer server, int size) {
41 | BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
42 | BufferedImage favicon = getServerFavicon(server);
43 | BufferedImage background = SERVER_BACKGROUND;
44 |
45 | // Create the graphics for drawing
46 | Graphics2D graphics = texture.createGraphics();
47 |
48 | // Set up the font
49 | graphics.setFont(Fonts.MINECRAFT);
50 |
51 | // Draw the background
52 | for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
53 | for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
54 | graphics.drawImage(background, backgroundX, backgroundY, null);
55 | }
56 | }
57 |
58 | int y = fontSize + 1;
59 | int x = 64 + 8;
60 | int initialX = x; // Store the initial value of x
61 |
62 | // Draw the favicon
63 | graphics.drawImage(favicon, padding, padding, null);
64 |
65 | // Draw the server hostname
66 | graphics.setColor(Color.WHITE);
67 | graphics.drawString(server.getHostname(), x, y);
68 |
69 | // Draw the server motd
70 | y += fontSize + (padding * 2);
71 | for (String line : server.getMotd().getRaw()) {
72 | int index = 0;
73 | int colorIndex = line.indexOf("§");
74 | while (colorIndex != -1) {
75 | // Draw text before color code
76 | String textBeforeColor = line.substring(index, colorIndex);
77 | graphics.drawString(textBeforeColor, x, y);
78 | // Calculate width of text before color code
79 | int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
80 | // Move x position to after the drawn text
81 | x += textWidth;
82 | // Set color based on color code
83 | char colorCode = Character.toLowerCase(line.charAt(colorIndex + 1));
84 |
85 | // Set the color and font style
86 | switch (colorCode) {
87 | case 'l': graphics.setFont(Fonts.MINECRAFT_BOLD);
88 | case 'o': graphics.setFont(Fonts.MINECRAFT_ITALIC);
89 | default: {
90 | try {
91 | graphics.setFont(Fonts.MINECRAFT);
92 | Color color = ColorUtils.getMinecraftColor(colorCode);
93 | graphics.setColor(color);
94 | } catch (Exception ignored) {
95 | // Unknown color, can ignore the error
96 | }
97 | }
98 | }
99 |
100 | // Move index to after the color code
101 | index = colorIndex + 2;
102 | // Find next color code
103 | colorIndex = line.indexOf("§", index);
104 | }
105 | // Draw remaining text
106 | String remainingText = line.substring(index);
107 | graphics.drawString(remainingText, x, y);
108 | // Move to the next line
109 | y += fontSize + padding;
110 | // Reset x position for the next line
111 | x = initialX; // Reset x to its initial value
112 | }
113 |
114 | // Ensure the font is reset
115 | graphics.setFont(Fonts.MINECRAFT);
116 |
117 | // Render the ping
118 | BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
119 | x = width - pingIcon.getWidth() - padding;
120 | graphics.drawImage(pingIcon, x, padding, null);
121 |
122 | // Reset the y position
123 | y = fontSize + 1;
124 |
125 | // Render the player count
126 | MinecraftServer.Players players = server.getPlayers();
127 | String playersOnline = players.getOnline() + "";
128 | String playersMax = players.getMax() + "";
129 |
130 | // Calculate the width of each player count element
131 | int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
132 | int slashWidth = graphics.getFontMetrics().stringWidth("/");
133 | int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
134 |
135 | // Calculate the total width of the player count string
136 | int totalWidth = maxWidth + slashWidth + onlineWidth;
137 |
138 | // Calculate the starting x position
139 | int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
140 |
141 | // Render the player count elements
142 | graphics.setColor(Color.LIGHT_GRAY);
143 | graphics.drawString(playersOnline, startX, y);
144 | startX += onlineWidth;
145 | graphics.setColor(Color.DARK_GRAY);
146 | graphics.drawString("/", startX, y);
147 | startX += slashWidth;
148 | graphics.setColor(Color.LIGHT_GRAY);
149 | graphics.drawString(playersMax, startX, y);
150 |
151 | return ImageUtils.resize(texture, (double) size / width);
152 | }
153 |
154 | /**
155 | * Get the favicon of a server.
156 | *
157 | * @param server the server to get the favicon of
158 | * @return the server favicon
159 | */
160 | public BufferedImage getServerFavicon(MinecraftServer server) {
161 | String favicon = null;
162 |
163 | // Get the server favicon
164 | if (server instanceof JavaMinecraftServer javaServer) {
165 | if (javaServer.getFavicon() != null) {
166 | favicon = javaServer.getFavicon().getBase64();
167 | }
168 | }
169 |
170 | // Fallback to the default server icon
171 | if (favicon == null) {
172 | favicon = ServerService.DEFAULT_SERVER_ICON;
173 | }
174 | return ImageUtils.base64ToImage(favicon);
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/BodyRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer.impl.skin;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.extern.log4j.Log4j2;
6 | import xyz.mcutils.backend.common.ImageUtils;
7 | import xyz.mcutils.backend.common.renderer.SkinRenderer;
8 | import xyz.mcutils.backend.model.skin.ISkinPart;
9 | import xyz.mcutils.backend.model.skin.Skin;
10 |
11 | import java.awt.*;
12 | import java.awt.image.BufferedImage;
13 |
14 | @AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Body")
15 | public class BodyRenderer extends SkinRenderer {
16 | public static final BodyRenderer INSTANCE = new BodyRenderer();
17 |
18 | @Override
19 | public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
20 | BufferedImage texture = new BufferedImage(16, 32, BufferedImage.TYPE_INT_ARGB); // The texture to return
21 | Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
22 |
23 | // Get the Vanilla skin parts to draw
24 | BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, -1, renderOverlays);
25 | BufferedImage body = getVanillaSkinPart(skin, ISkinPart.Vanilla.BODY_FRONT, -1, renderOverlays);
26 | BufferedImage leftArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_ARM_FRONT, -1, renderOverlays);
27 | BufferedImage rightArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_ARM_FRONT, -1, renderOverlays);
28 | BufferedImage leftLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_LEG_FRONT, -1, renderOverlays);
29 | BufferedImage rightLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_LEG_FRONT, -1, renderOverlays);
30 |
31 | // Draw the body parts
32 | graphics.drawImage(face, 4, 0, null);
33 | graphics.drawImage(body, 4, 8, null);
34 | graphics.drawImage(leftArm, skin.getModel() == Skin.Model.SLIM ? 1 : 0, 8, null);
35 | graphics.drawImage(rightArm, 12, 8, null);
36 | graphics.drawImage(leftLeg, 8, 20, null);
37 | graphics.drawImage(rightLeg, 4, 20, null);
38 |
39 | graphics.dispose();
40 | return ImageUtils.resize(texture, (double) size / 32);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer.impl.skin;
2 |
3 | import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
4 | import xyz.mcutils.backend.model.skin.ISkinPart;
5 | import xyz.mcutils.backend.model.skin.Skin;
6 |
7 | import java.awt.*;
8 | import java.awt.geom.AffineTransform;
9 | import java.awt.image.BufferedImage;
10 |
11 | public class IsometricHeadRenderer extends IsometricSkinRenderer {
12 | public static final IsometricHeadRenderer INSTANCE = new IsometricHeadRenderer();
13 |
14 | private static final double SKEW_A = 26D / 45D; // 0.57777777
15 | private static final double SKEW_B = SKEW_A * 2D; // 1.15555555
16 |
17 | private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0);
18 | private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A);
19 | private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D);
20 |
21 | @Override
22 | public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
23 | double scale = (size / 8D) / 2.5;
24 | double zOffset = scale * 3.5D;
25 | double xOffset = scale * 2D;
26 |
27 | BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
28 | Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
29 |
30 | // Get the Vanilla skin parts to draw
31 | BufferedImage headTop = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_TOP, scale, renderOverlays);
32 | BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, scale, renderOverlays);
33 | BufferedImage headLeft = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_LEFT, scale, renderOverlays);
34 |
35 | // Draw the top head part
36 | drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2);
37 |
38 | // Draw the face part
39 | double x = xOffset + 8 * scale;
40 | drawPart(graphics, face, FACE_TRANSFORM, x, x + zOffset - 0.5, face.getWidth(), face.getHeight());
41 |
42 | // Draw the left head part
43 | drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight());
44 |
45 | graphics.dispose();
46 | return texture;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/SquareRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer.impl.skin;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.extern.log4j.Log4j2;
6 | import xyz.mcutils.backend.common.renderer.SkinRenderer;
7 | import xyz.mcutils.backend.model.skin.ISkinPart;
8 | import xyz.mcutils.backend.model.skin.Skin;
9 |
10 | import java.awt.*;
11 | import java.awt.image.BufferedImage;
12 |
13 | @AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Square")
14 | public class SquareRenderer extends SkinRenderer {
15 | public static final SquareRenderer INSTANCE = new SquareRenderer();
16 |
17 | @Override
18 | public BufferedImage render(Skin skin, ISkinPart.Vanilla part, boolean renderOverlays, int size) {
19 | double scale = size / 8D;
20 | BufferedImage partImage = getVanillaSkinPart(skin, part, scale, renderOverlays); // Get the part image
21 | if (!renderOverlays) { // Not rendering overlays
22 | return partImage;
23 | }
24 | // Create a new image, draw our skin part texture, and then apply overlays
25 | BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
26 | Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
27 | graphics.drawImage(partImage, 0, 0, null);
28 |
29 | graphics.dispose();
30 | return texture;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/config/Config.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.config;
2 |
3 | import jakarta.annotation.PostConstruct;
4 | import lombok.Getter;
5 | import lombok.NonNull;
6 | import lombok.extern.log4j.Log4j2;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.boot.web.servlet.FilterRegistrationBean;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Configuration;
12 | import org.springframework.core.env.Environment;
13 | import org.springframework.web.filter.ShallowEtagHeaderFilter;
14 | import org.springframework.web.servlet.config.annotation.CorsRegistry;
15 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
16 |
17 | @Getter @Log4j2(topic = "Config")
18 | @Configuration
19 | public class Config {
20 | public static Config INSTANCE;
21 |
22 | @Autowired
23 | private Environment environment;
24 |
25 | @Value("${public-url}")
26 | private String webPublicUrl;
27 |
28 | @PostConstruct
29 | public void onInitialize() {
30 | INSTANCE = this;
31 | }
32 |
33 | @Bean
34 | public FilterRegistrationBean shallowEtagHeaderFilter() {
35 | FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
36 | filterRegistrationBean.addUrlPatterns("/*");
37 | filterRegistrationBean.setName("etagFilter");
38 | return filterRegistrationBean;
39 | }
40 |
41 | @Bean
42 | public WebMvcConfigurer configureCors() {
43 | return new WebMvcConfigurer() {
44 | @Override
45 | public void addCorsMappings(@NonNull CorsRegistry registry) {
46 | // Allow all origins to access the API
47 | registry.addMapping("/**")
48 | .allowedOrigins("*") // Allow all origins
49 | .allowedMethods("*") // Allow all methods
50 | .allowedHeaders("*"); // Allow all headers
51 | }
52 | };
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/config/MongoConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
6 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
7 |
8 | @Configuration
9 | @EnableMongoRepositories(basePackages = "xyz.mcutils.backend.repository.mongo")
10 | public class MongoConfig {
11 | @Autowired
12 | void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
13 | mappingMongoConverter.setMapKeyDotReplacement("-DOT");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/config/OpenAPIConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.config;
2 |
3 | import io.swagger.v3.oas.models.OpenAPI;
4 | import io.swagger.v3.oas.models.info.Contact;
5 | import io.swagger.v3.oas.models.info.Info;
6 | import io.swagger.v3.oas.models.info.License;
7 | import io.swagger.v3.oas.models.servers.Server;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.boot.info.BuildProperties;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Configuration;
12 |
13 | import java.util.List;
14 |
15 | @Configuration
16 | public class OpenAPIConfiguration {
17 | /**
18 | * The build properties of the
19 | * app, null if the app is not built.
20 | */
21 | private final BuildProperties buildProperties;
22 |
23 | @Autowired
24 | public OpenAPIConfiguration(BuildProperties buildProperties) {
25 | this.buildProperties = buildProperties;
26 | }
27 |
28 | @Bean
29 | public OpenAPI defineOpenAPI() {
30 | Server server = new Server();
31 | server.setUrl(Config.INSTANCE.getWebPublicUrl());
32 |
33 | Contact contact = new Contact();
34 | contact.setName("Liam");
35 | contact.setEmail("liam@fascinated.cc");
36 | contact.setUrl("https://fascinated.cc");
37 |
38 | Info info = new Info();
39 | info.setTitle("Minecraft Utilities API");
40 | info.setVersion(buildProperties == null ? "N/A" : buildProperties.getVersion());
41 | info.setDescription("Wrapper for the Minecraft APIs to make them easier to use.");
42 | info.setContact(contact);
43 | info.setLicense(new License().name("MIT License").url("https://opensource.org/licenses/MIT"));
44 |
45 | return new OpenAPI().servers(List.of(server)).info(info);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/config/RedisConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.config;
2 |
3 | import lombok.NonNull;
4 | import lombok.extern.log4j.Log4j2;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
9 | import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
10 | import org.springframework.data.redis.core.RedisTemplate;
11 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
12 |
13 | /**
14 | * @author Braydon
15 | */
16 | @Configuration
17 | @Log4j2(topic = "Redis")
18 | @EnableRedisRepositories(basePackages = "xyz.mcutils.backend.repository.redis")
19 | public class RedisConfig {
20 | /**
21 | * The Redis server host.
22 | */
23 | @Value("${spring.data.redis.host}")
24 | private String host;
25 |
26 | /**
27 | * The Redis server port.
28 | */
29 | @Value("${spring.data.redis.port}")
30 | private int port;
31 |
32 | /**
33 | * The Redis database index.
34 | */
35 | @Value("${spring.data.redis.database}")
36 | private int database;
37 |
38 | /**
39 | * The optional Redis password.
40 | */
41 | @Value("${spring.data.redis.auth}")
42 | private String auth;
43 |
44 | /**
45 | * Build the config to use for Redis.
46 | *
47 | * @return the config
48 | * @see RedisTemplate for config
49 | */
50 | @Bean @NonNull
51 | public RedisTemplate redisTemplate() {
52 | RedisTemplate template = new RedisTemplate<>();
53 | template.setConnectionFactory(jedisConnectionFactory());
54 | return template;
55 | }
56 |
57 | /**
58 | * Build the connection factory to use
59 | * when making connections to Redis.
60 | *
61 | * @return the built factory
62 | * @see JedisConnectionFactory for factory
63 | */
64 | @Bean @NonNull
65 | public JedisConnectionFactory jedisConnectionFactory() {
66 | log.info("Connecting to Redis at {}:{}/{}", host, port, database);
67 | RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
68 | config.setDatabase(database);
69 | if (!auth.trim().isEmpty()) { // Auth with our provided password
70 | log.info("Using auth...");
71 | config.setPassword(auth);
72 | }
73 | return new JedisConnectionFactory(config);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/HealthController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import org.springframework.http.ResponseEntity;
4 | import org.springframework.stereotype.Controller;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 |
8 | import java.util.Map;
9 |
10 | @Controller
11 | @RequestMapping(value = "/")
12 | public class HealthController {
13 |
14 | @GetMapping(value = "/health")
15 | public ResponseEntity> home() {
16 | return ResponseEntity.ok(Map.of(
17 | "status", "OK"
18 | ));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/HomeController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.ui.Model;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 | import xyz.mcutils.backend.config.Config;
8 |
9 | @Controller
10 | @RequestMapping(value = "/")
11 | public class HomeController {
12 | private final String examplePlayer = "Notch";
13 | private final String exampleJavaServer = "aetheria.cc";
14 | private final String exampleBedrockServer = "geo.hivebedrock.network";
15 |
16 | @GetMapping(value = "/")
17 | public String home(Model model) {
18 | String publicUrl = Config.INSTANCE.getWebPublicUrl();
19 |
20 | model.addAttribute("public_url", publicUrl);
21 | model.addAttribute("player_example_url", publicUrl + "/player/" + examplePlayer);
22 | model.addAttribute("java_server_example_url", publicUrl + "/server/java/" + exampleJavaServer);
23 | model.addAttribute("bedrock_server_example_url", publicUrl + "/server/bedrock/" + exampleBedrockServer);
24 | model.addAttribute("mojang_endpoint_status_url", publicUrl + "/mojang/status");
25 | model.addAttribute("swagger_url", publicUrl + "/swagger-ui.html");
26 | return "index";
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/MojangController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import io.swagger.v3.oas.annotations.tags.Tag;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.http.CacheControl;
6 | import org.springframework.http.MediaType;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.ResponseBody;
11 | import org.springframework.web.bind.annotation.RestController;
12 | import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
13 | import xyz.mcutils.backend.service.MojangService;
14 |
15 | import java.util.Map;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | @RestController
19 | @RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
20 | @Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
21 | public class MojangController {
22 | @Autowired
23 | private MojangService mojangService;
24 |
25 | @ResponseBody
26 | @GetMapping(value = "/status")
27 | public ResponseEntity> getStatus() {
28 | return ResponseEntity.ok()
29 | .cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
30 | .body(Map.of("endpoints", mojangService.getMojangServerStatus()));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/PlayerController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import io.swagger.v3.oas.annotations.Parameter;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.http.CacheControl;
7 | import org.springframework.http.HttpHeaders;
8 | import org.springframework.http.MediaType;
9 | import org.springframework.http.ResponseEntity;
10 | import org.springframework.web.bind.annotation.*;
11 | import xyz.mcutils.backend.model.cache.CachedPlayer;
12 | import xyz.mcutils.backend.model.cache.CachedPlayerName;
13 | import xyz.mcutils.backend.model.player.Player;
14 | import xyz.mcutils.backend.service.PlayerService;
15 |
16 | import java.util.concurrent.TimeUnit;
17 |
18 | @RestController
19 | @RequestMapping(value = "/player/")
20 | @Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
21 | public class PlayerController {
22 |
23 | private final PlayerService playerService;
24 |
25 | @Autowired
26 | public PlayerController(PlayerService playerManagerService) {
27 | this.playerService = playerManagerService;
28 | }
29 |
30 | @ResponseBody
31 | @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
32 | public ResponseEntity> getPlayer(
33 | @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
34 | CachedPlayer player = playerService.getPlayer(id);
35 |
36 | return ResponseEntity.ok()
37 | .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
38 | .body(player);
39 | }
40 |
41 | @ResponseBody
42 | @GetMapping(value = "/uuid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
43 | public ResponseEntity getPlayerUuid(
44 | @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
45 | CachedPlayerName player = playerService.usernameToUuid(id);
46 |
47 | return ResponseEntity.ok()
48 | .cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePublic())
49 | .body(player);
50 | }
51 |
52 | @GetMapping(value = "/{part}/{id}")
53 | public ResponseEntity> getPlayerHead(
54 | @Parameter(description = "The part of the skin", example = "head") @PathVariable String part,
55 | @Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id,
56 | @Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size,
57 | @Parameter(description = "Whether to render the skin overlay (skin layers)", example = "false") @RequestParam(required = false, defaultValue = "false") boolean overlays,
58 | @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
59 | CachedPlayer cachedPlayer = playerService.getPlayer(id);
60 | Player player = cachedPlayer.getPlayer();
61 | String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
62 |
63 | // Return the part image
64 | return ResponseEntity.ok()
65 | .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
66 | .contentType(MediaType.IMAGE_PNG)
67 | .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
68 | .body(playerService.getSkinPart(player, part, overlays, size).getBytes());
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/ServerController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import io.swagger.v3.oas.annotations.Parameter;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.http.CacheControl;
7 | import org.springframework.http.HttpHeaders;
8 | import org.springframework.http.MediaType;
9 | import org.springframework.http.ResponseEntity;
10 | import org.springframework.web.bind.annotation.*;
11 | import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
12 | import xyz.mcutils.backend.service.MojangService;
13 | import xyz.mcutils.backend.service.ServerService;
14 |
15 | import java.util.Map;
16 | import java.util.concurrent.TimeUnit;
17 |
18 | @RestController
19 | @RequestMapping(value = "/server/")
20 | @Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
21 | public class ServerController {
22 |
23 | private final ServerService serverService;
24 | private final MojangService mojangService;
25 |
26 | @Autowired
27 | public ServerController(ServerService serverService, MojangService mojangService) {
28 | this.serverService = serverService;
29 | this.mojangService = mojangService;
30 | }
31 |
32 | @ResponseBody
33 | @GetMapping(value = "/{platform}/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
34 | public ResponseEntity getServer(
35 | @Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
36 | @Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname) {
37 | CachedMinecraftServer server = serverService.getServer(platform, hostname);
38 |
39 | return ResponseEntity.ok()
40 | .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
41 | .body(server);
42 | }
43 |
44 | @ResponseBody
45 | @GetMapping(value = "/icon/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
46 | public ResponseEntity getServerIcon(
47 | @Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
48 | @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
49 | String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
50 | byte[] favicon = serverService.getServerFavicon(hostname);
51 |
52 | return ResponseEntity.ok()
53 | .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
54 | .contentType(MediaType.IMAGE_PNG)
55 | .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
56 | .body(favicon);
57 | }
58 |
59 | @ResponseBody
60 | @GetMapping(value = "/{platform}/preview/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
61 | public ResponseEntity getServerPreview(
62 | @Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
63 | @Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
64 | @Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download,
65 | @Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) {
66 | String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
67 | CachedMinecraftServer server = serverService.getServer(platform, hostname);
68 |
69 | return ResponseEntity.ok()
70 | .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
71 | .contentType(MediaType.IMAGE_PNG)
72 | .header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
73 | .body(serverService.getServerPreview(server, platform, size));
74 | }
75 |
76 | @ResponseBody
77 | @GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
78 | public ResponseEntity> getServerBlockedStatus(
79 | @Parameter(description = "The hostname of the server", example = "aetheria.cc") @PathVariable String hostname) {
80 | return ResponseEntity.ok()
81 | .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
82 | .body(Map.of(
83 | "blocked", mojangService.isServerBlocked(hostname)
84 | ));
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/ExceptionControllerAdvice.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import io.sentry.Sentry;
5 | import org.springframework.http.HttpStatus;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.web.bind.annotation.ControllerAdvice;
8 | import org.springframework.web.bind.annotation.ExceptionHandler;
9 | import org.springframework.web.bind.annotation.ResponseStatus;
10 | import org.springframework.web.servlet.resource.NoResourceFoundException;
11 | import xyz.mcutils.backend.model.response.ErrorResponse;
12 |
13 | @ControllerAdvice
14 | public final class ExceptionControllerAdvice {
15 |
16 | /**
17 | * Handle a raised exception.
18 | *
19 | * @param ex the raised exception
20 | * @return the error response
21 | */
22 | @ExceptionHandler(Exception.class)
23 | public ResponseEntity> handleException(@NonNull Exception ex) {
24 | HttpStatus status = null; // Get the HTTP status
25 | if (ex instanceof NoResourceFoundException) { // Not found
26 | status = HttpStatus.NOT_FOUND;
27 | } else if (ex instanceof UnsupportedOperationException) { // Not implemented
28 | status = HttpStatus.NOT_IMPLEMENTED;
29 | }
30 | if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation
31 | status = ex.getClass().getAnnotation(ResponseStatus.class).value();
32 | }
33 | String message = ex.getLocalizedMessage(); // Get the error message
34 | if (message == null) { // Fallback
35 | message = "An internal error has occurred.";
36 | }
37 | // Print the stack trace if no response status is present
38 | if (status == null) {
39 | ex.printStackTrace();
40 | }
41 | if (status == null) { // Fallback to 500
42 | status = HttpStatus.INTERNAL_SERVER_ERROR;
43 | Sentry.captureException(ex); // Capture the exception with Sentry
44 | }
45 | return new ResponseEntity<>(new ErrorResponse(status, message), status);
46 | }
47 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.BAD_REQUEST)
7 | public class BadRequestException extends RuntimeException {
8 |
9 | public BadRequestException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/InternalServerErrorException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7 | public class InternalServerErrorException extends RuntimeException {
8 |
9 | public InternalServerErrorException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/MojangAPIRateLimitException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 |
4 | public class MojangAPIRateLimitException extends RateLimitException {
5 |
6 | public MojangAPIRateLimitException() {
7 | super("Mojang API rate limit exceeded. Please try again later.");
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/RateLimitException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
7 | public class RateLimitException extends RuntimeException {
8 |
9 | public RateLimitException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import lombok.experimental.StandardException;
4 | import org.springframework.http.HttpStatus;
5 | import org.springframework.web.bind.annotation.ResponseStatus;
6 |
7 | @StandardException
8 | @ResponseStatus(HttpStatus.NOT_FOUND)
9 | public class ResourceNotFoundException extends RuntimeException { }
10 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/log/TransactionLogger.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.log;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import lombok.NonNull;
5 | import lombok.extern.slf4j.Slf4j;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.core.MethodParameter;
8 | import org.springframework.http.MediaType;
9 | import org.springframework.http.converter.HttpMessageConverter;
10 | import org.springframework.http.server.ServerHttpRequest;
11 | import org.springframework.http.server.ServerHttpResponse;
12 | import org.springframework.http.server.ServletServerHttpRequest;
13 | import org.springframework.web.bind.annotation.ControllerAdvice;
14 | import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
15 | import xyz.mcutils.backend.common.IPUtils;
16 | import xyz.mcutils.backend.service.MetricService;
17 | import xyz.mcutils.backend.service.metric.metrics.RequestsPerRouteMetric;
18 | import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
19 |
20 | import java.util.Arrays;
21 | import java.util.HashMap;
22 | import java.util.Map;
23 | import java.util.Map.Entry;
24 |
25 | @ControllerAdvice
26 | @Slf4j(topic = "Req Transaction")
27 | public class TransactionLogger implements ResponseBodyAdvice