forked from MinecraftUtilities/Backend
Compare commits
307 Commits
Author | SHA1 | Date | |
---|---|---|---|
0bc614ce39 | |||
499c54c8cf | |||
41f7ca07b0 | |||
5ec61940ac | |||
6665e8a655 | |||
07562eb94d | |||
a78adf67c7 | |||
fc1f51da75 | |||
c796875d8c | |||
82fb2a3d23 | |||
2e326bb7be | |||
c5bf941c54 | |||
0eb965a26d | |||
5481c9302c | |||
b7834ab389 | |||
bb651bd88b | |||
2b017f9ef7 | |||
d83391de33 | |||
76bef70473 | |||
4aa5b0a90d | |||
6a44618ae9 | |||
146d053af8 | |||
796146c039 | |||
00c83d9ae3 | |||
3bbab24e45 | |||
5034a11e63 | |||
3d11c65678 | |||
fd3da02159 | |||
0bdaefe4a2 | |||
ba167b4e56 | |||
493e7ce4c0 | |||
7f501431b1 | |||
fa92791b56 | |||
6750773640 | |||
cecc6bc94f | |||
20db1c1aff | |||
e62e7f0fc2 | |||
c9ed681204 | |||
c6642e85fe | |||
9dfbd1af47 | |||
c8629e4f27 | |||
a6209c45ff | |||
ee5b1f12d8 | |||
ff79372ead | |||
f0e1490463 | |||
9196ec3578 | |||
a8558578f2 | |||
67efda71d2 | |||
2ad5556041 | |||
3faf2d3319 | |||
bf992713dc | |||
23e240fce1 | |||
cca45057f0 | |||
beda7fa230 | |||
d394c21f69 | |||
7df4fda744 | |||
f85ed49545 | |||
69833bf560 | |||
6693fc6793 | |||
a6ea3ab143 | |||
f96e5d5426 | |||
e360ad4446 | |||
c913816447 | |||
5dccce9fc5 | |||
3cd5e32118 | |||
5ca707bef1 | |||
6fc02cd906 | |||
f4a9d7c31c | |||
7127794152 | |||
f664406299 | |||
c5b5b3b105 | |||
b5fa470801 | |||
0854c9e76a | |||
17803410bd | |||
92fe2b28e3 | |||
eae027af84 | |||
d2ae4b4cc5 | |||
ff58b1756a | |||
543aff2a04 | |||
b666e5a8b7 | |||
ab3ed0511f | |||
bf44c9bc0d | |||
cf8e27f039 | |||
e5935c6696 | |||
5871c64582 | |||
1cfcce4806 | |||
46d4a53b11 | |||
d0cfd03ad9 | |||
4dc263961d | |||
8a8c6b542a | |||
5b8017e403 | |||
ad83e270b6 | |||
aa69970ec7 | |||
7ecaf8c580 | |||
8a985b52b8 | |||
03c679d25c | |||
6096764905 | |||
984dd8bfdc | |||
806620afff | |||
0b6c752441 | |||
f2d6200bd4 | |||
bee6f2d52d | |||
4cd345cb8f | |||
2e90f8b041 | |||
5f764d16dc | |||
4fa94c7264 | |||
7a5b42e9d7 | |||
547fa075f3 | |||
8dcde443ee | |||
ba699f5305 | |||
b3e560d1e2 | |||
cb9181010a | |||
046df7fd1f | |||
3ac4bfe2ee | |||
f037f3f9e7 | |||
04b99715c1 | |||
4e5258d74c | |||
6e336bb879 | |||
daf3770b73 | |||
02eb9e2a2e | |||
aa87d2f374 | |||
1eb540380e | |||
b0bdf6e800 | |||
597c3850e3 | |||
ce3067ee0e | |||
1c685ca414 | |||
c91a4afdf9 | |||
4fd66dffd3 | |||
b93c7f68fb | |||
d9ebbfe99e | |||
c58ceac5e4 | |||
e2d97ae417 | |||
d764242aed | |||
ebe5e763fa | |||
7e436c917a | |||
3069e9c9e8 | |||
036d8439ba | |||
0228f7205d | |||
c46443425e | |||
9fd84f2e5b | |||
c5758d38e0 | |||
cbfaa867a9 | |||
5fbab2c0ec | |||
3d17798f30 | |||
a47db6d843 | |||
ecca157d86 | |||
632d33197d | |||
8f758820e1 | |||
d8d4f32006 | |||
7a50dccabd | |||
be40a981fe | |||
08be02ff73 | |||
37b048c2a9 | |||
b7b788847d | |||
924e045a18 | |||
bedde3b0eb | |||
45fb517385 | |||
5c77b59b90 | |||
65aa5b102b | |||
2be48c7c30 | |||
375a8cc2e6 | |||
2e5c8b1ee0 | |||
f3b1104e93 | |||
1d597d5d92 | |||
5877b100bc | |||
3b946132af | |||
9453e91892 | |||
a6d06e6873 | |||
2b35fd51b0 | |||
72ac874b9f | |||
fa0189c421 | |||
cf618b3123 | |||
1675fe16d2 | |||
15d9dcbd5d | |||
1a518b3d00 | |||
1de25a92ba | |||
4d0ae5286d | |||
1408cecee3 | |||
13964e0f65 | |||
6cea5a1f29 | |||
05d0aa500c | |||
8d45ba8cbd | |||
e9da32775f | |||
fc640fe1a0 | |||
e067d399c8 | |||
69321adb1f | |||
595ca5b36a | |||
88c940431b | |||
a15326a847 | |||
941ffaf6ae | |||
b14c969013 | |||
c72bda317e | |||
499fcb6fa5 | |||
edb02c2ba1 | |||
a1755948c1 | |||
15885f7e00 | |||
1a0dd8844d | |||
77f787b659 | |||
a966977d82 | |||
bda70b19a8 | |||
e5e3503abc | |||
b5e8664ad3 | |||
8216ec7943 | |||
c198339acc | |||
2895525412 | |||
348edfd1ef | |||
66f5660274 | |||
55c2c95269 | |||
77bd9a7c7e | |||
0b187a852c | |||
4d4e8557d8 | |||
b708191267 | |||
3cf16bd2eb | |||
852f5a8bea | |||
57601acbb1 | |||
471c3e6e80 | |||
811ea348cf | |||
0c8f769ee7 | |||
586539d810 | |||
52d89a6d9f | |||
5381a2887e | |||
4ababb0cb5 | |||
4f4a06a4fb | |||
53c50b3a05 | |||
f1dae95a4c | |||
f4cf93cf08 | |||
157bdf5e5a | |||
4b672de85d | |||
55c1ca4139 | |||
4e08955ab9 | |||
e788ae003f | |||
a1ad295e0c | |||
2ea58d8080 | |||
83a95fb26c | |||
9debdece9e | |||
9acb7c259a | |||
d9e6becebb | |||
654037c8e1 | |||
977a1dcbc4 | |||
bfbaf34b24 | |||
c8b6f2aad8 | |||
9b7b761ffd | |||
f63d1cc3ec | |||
1a74b0099b | |||
4f26110405 | |||
6f840654e9 | |||
e9fa275002 | |||
b682153ebb | |||
a16fda1b53 | |||
8e5adf337a | |||
557c0facb7 | |||
a11a90f530 | |||
f2e8360567 | |||
944000ab1a | |||
bd09539732 | |||
4a9149e41e | |||
a3b9cb5e77 | |||
624dcc0be6 | |||
0ea69f86f9 | |||
d156d2cb3b | |||
8f1bc67596 | |||
de338fed82 | |||
9d846dec1d | |||
c9a4e2d4ea | |||
b26d5aa67f | |||
795c97401c | |||
cd3738a2b9 | |||
63a3587586 | |||
28cd7f192d | |||
3790d4a312 | |||
7855b5bcca | |||
b1bcaad8dc | |||
2ff9122a99 | |||
cee9cfe01c | |||
0b8c3cbc83 | |||
48c9b66fd1 | |||
11079ea572 | |||
2ba9651161 | |||
5ad2f438d1 | |||
23bcb1d76e | |||
c8c4c8ad3e | |||
78c3333038 | |||
146cc30413 | |||
e26a34fb74 | |||
c0c4e32809 | |||
cb7c2e162f | |||
ac8b2e4b74 | |||
50d4b2df86 | |||
50ae9b47be | |||
3bd0ea3838 | |||
708ccc294d | |||
67ada952ca | |||
3e2bcbe922 | |||
8990a6308a | |||
d959169f0b | |||
2e58d9c925 | |||
330c3efc78 | |||
c7fe26ef8f | |||
d525d343e4 | |||
b011fdbede | |||
3a3a2e223f | |||
2f09b55f33 | |||
1255cbbcd4 | |||
f3522287fc | |||
fcb8ef0357 | |||
25c69e11e1 | |||
ed3b7e3064 |
@ -1,32 +1,49 @@
|
|||||||
name: "ci"
|
name: Deploy App
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ["master"]
|
||||||
- master
|
paths-ignore:
|
||||||
|
- .gitignore
|
||||||
|
- README.md
|
||||||
|
- LICENSE
|
||||||
|
- docker-compose.yml
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ["ubuntu-latest"]
|
||||||
|
git-version: ["2.44.0"]
|
||||||
|
java-version: ["17"]
|
||||||
|
maven-version: ["3.8.5"]
|
||||||
|
runs-on: ${{ matrix.arch }}
|
||||||
|
|
||||||
|
# Steps to run
|
||||||
steps:
|
steps:
|
||||||
- name: Cloning repo
|
# Checkout the repo
|
||||||
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17 and Maven
|
# Setup Java and Maven
|
||||||
uses: s4u/setup-maven-action@v1.7.0
|
- name: Set up JDK and Maven
|
||||||
|
uses: s4u/setup-maven-action@v1.14.0
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: ${{ matrix.java-version }}
|
||||||
distribution: 'temurin'
|
distribution: "zulu"
|
||||||
maven-version: 3.8.5
|
maven-version: ${{ matrix.maven-version }}
|
||||||
|
|
||||||
|
# Run JUnit Tests
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: mvn --batch-mode test -q
|
run: mvn --batch-mode test -q
|
||||||
|
|
||||||
- name: Cloning repo
|
# Re-checkout to reset the FS before deploying to Dokku
|
||||||
|
- name: Checkout - Reset FS
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Deploy to Dokku
|
||||||
- name: Push to dokku
|
- name: Push to dokku
|
||||||
uses: dokku/github-action@master
|
uses: dokku/github-action@master
|
||||||
with:
|
with:
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -28,3 +28,7 @@ fabric.properties
|
|||||||
git.properties
|
git.properties
|
||||||
pom.xml.versionsBackup
|
pom.xml.versionsBackup
|
||||||
application.yml
|
application.yml
|
||||||
|
target/
|
||||||
|
|
||||||
|
### MaxMind GeoIP2
|
||||||
|
data/
|
||||||
|
13
Dockerfile
13
Dockerfile
@ -1,4 +1,8 @@
|
|||||||
FROM maven:3.8.5-openjdk-17-slim
|
FROM maven:3.9.8-eclipse-temurin-17-alpine
|
||||||
|
|
||||||
|
RUN apk --update --upgrade --no-cache add fontconfig ttf-freefont font-noto terminus-font \
|
||||||
|
&& fc-cache -f \
|
||||||
|
&& fc-list | sort
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /home/container
|
WORKDIR /home/container
|
||||||
@ -7,11 +11,14 @@ WORKDIR /home/container
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the jar
|
# Build the jar
|
||||||
RUN mvn package -q
|
RUN mvn package -q -Dmaven.test.skip -DskipTests -T2C
|
||||||
|
|
||||||
# Make port 80 available to the world outside this container
|
# Make port 80 available to the world outside this container
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
|
|
||||||
|
# Indicate that we're running in production
|
||||||
|
ENV ENVIRONMENT=production
|
||||||
|
|
||||||
# Run the jar file
|
# Run the jar file
|
||||||
CMD ["java", "-jar", "target/Minecraft-Helper-1.0-SNAPSHOT.jar"]
|
CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024, Fascinated
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Minecraft Utilities - Backend
|
||||||
|
|
||||||
|
See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.
|
7
influx-commands.md
Normal file
7
influx-commands.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Useful InfluxDB commands
|
||||||
|
|
||||||
|
## Delete data from bucket
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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"
|
||||||
|
```
|
185
pom.xml
185
pom.xml
@ -5,8 +5,8 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>cc.fascinated</groupId>
|
<groupId>cc.fascinated</groupId>
|
||||||
<artifactId>Minecraft-Helper</artifactId>
|
<artifactId>Minecraft-Utilities</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>17</maven.compiler.source>
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
@ -17,58 +17,89 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.2.4</version>
|
<version>3.3.2</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<!-- Build Steps -->
|
||||||
<build>
|
<build>
|
||||||
|
<finalName>${project.artifactId}</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<!-- Spring -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>build-info</id>
|
||||||
|
<goals>
|
||||||
|
<goal>build-info</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<additionalProperties>
|
||||||
|
<description>${project.description}</description>
|
||||||
|
</additionalProperties>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
<dependencies>
|
<repositories>
|
||||||
<dependency>
|
<!-- Jitpack - Used for dnsjava -->
|
||||||
<groupId>org.projectlombok</groupId>
|
<repository>
|
||||||
<artifactId>lombok</artifactId>
|
<id>jitpack.io</id>
|
||||||
<version>1.18.32</version>
|
<url>https://jitpack.io</url>
|
||||||
<scope>provided</scope>
|
</repository>
|
||||||
</dependency>
|
</repositories>
|
||||||
|
|
||||||
<dependency>
|
<dependencies>
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
<!-- Spring -->
|
||||||
<artifactId>log4j-api</artifactId>
|
|
||||||
<version>2.20.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-core</artifactId>
|
|
||||||
<version>2.20.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.yaml</groupId>
|
|
||||||
<artifactId>snakeyaml</artifactId>
|
|
||||||
<version>2.2</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.gson</groupId>
|
|
||||||
<artifactId>gson</artifactId>
|
|
||||||
<version>2.10.1</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Websockets -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis for caching -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>io.lettuce</groupId>
|
||||||
|
<artifactId>lettuce-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>redis.clients</groupId>
|
||||||
|
<artifactId>jedis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MongoDB for data storage -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Libraries -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.34</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.11.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -77,37 +108,95 @@
|
|||||||
<version>0.5.11</version>
|
<version>0.5.11</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.md-5</groupId>
|
||||||
|
<artifactId>bungeecord-chat</artifactId>
|
||||||
|
<version>1.20-R0.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
<artifactId>httpclient5</artifactId>
|
<artifactId>httpclient5</artifactId>
|
||||||
<version>5.3.1</version>
|
<version>5.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Sentry -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.sentry</groupId>
|
||||||
|
<artifactId>sentry-spring-boot-starter-jakarta</artifactId>
|
||||||
|
<version>7.14.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- InfluxDB Metrics -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.influxdb</groupId>
|
||||||
|
<artifactId>influxdb-spring</artifactId>
|
||||||
|
<version>7.2.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.influxdb</groupId>
|
||||||
|
<artifactId>influxdb-client-java</artifactId>
|
||||||
|
<version>7.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DNS Lookup -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.dnsjava</groupId>
|
||||||
|
<artifactId>dnsjava</artifactId>
|
||||||
|
<version>v3.5.2</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Unit Tests -->
|
<!-- SwaggerUI -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>junit-jupiter-engine</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>5.10.2</version>
|
<version>2.6.0</version>
|
||||||
<scope>test</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- GeoIP - IP Lookups -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>com.maxmind.geoip2</groupId>
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
<artifactId>geoip2</artifactId>
|
||||||
<version>5.10.2</version>
|
<version>4.2.0</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Archive Utilities -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework</groupId>
|
<groupId>org.codehaus.plexus</groupId>
|
||||||
<artifactId>spring-test</artifactId>
|
<artifactId>plexus-archiver</artifactId>
|
||||||
<version>6.1.5</version>
|
<version>4.10.0</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tests -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.codemonstur</groupId>
|
||||||
|
<artifactId>embedded-redis</artifactId>
|
||||||
|
<version>1.4.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.flapdoodle.embed</groupId>
|
||||||
|
<artifactId>de.flapdoodle.embed.mongo.spring3x</artifactId>
|
||||||
|
<version>4.16.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
6
renovate.json
Normal file
6
renovate.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
@ -1,72 +0,0 @@
|
|||||||
package cc.fascinated.common;
|
|
||||||
|
|
||||||
import cc.fascinated.Main;
|
|
||||||
import cc.fascinated.model.player.Skin;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
|
|
||||||
@UtilityClass @Log4j2
|
|
||||||
public class PlayerUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the skin data from the URL.
|
|
||||||
*
|
|
||||||
* @return the skin data
|
|
||||||
*/
|
|
||||||
@SneakyThrows
|
|
||||||
@JsonIgnore
|
|
||||||
public static BufferedImage getSkinImage(String url) {
|
|
||||||
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
|
|
||||||
HttpResponse.BodyHandlers.ofByteArray());
|
|
||||||
byte[] body = response.body();
|
|
||||||
if (body == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ImageIO.read(new ByteArrayInputStream(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the part data from the skin.
|
|
||||||
*
|
|
||||||
* @return the part data
|
|
||||||
*/
|
|
||||||
public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) {
|
|
||||||
if (size == -1) {
|
|
||||||
size = part.getDefaultSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
BufferedImage image = skin.getSkinImage();
|
|
||||||
if (image == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Get the part of the image (e.g. the head)
|
|
||||||
BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight());
|
|
||||||
|
|
||||||
// Scale the image
|
|
||||||
BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType());
|
|
||||||
Graphics2D graphics2D = scaledImage.createGraphics();
|
|
||||||
graphics2D.drawImage(partImage, 0, 0, size, size, null);
|
|
||||||
graphics2D.dispose();
|
|
||||||
partImage = scaledImage;
|
|
||||||
|
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(partImage, "png", byteArrayOutputStream);
|
|
||||||
return byteArrayOutputStream.toByteArray();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package cc.fascinated.common;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class UUIDUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add dashes to a UUID.
|
|
||||||
*
|
|
||||||
* @param idNoDashes the UUID without dashes
|
|
||||||
* @return the UUID with dashes
|
|
||||||
*/
|
|
||||||
public static String addUuidDashes(String idNoDashes) {
|
|
||||||
StringBuilder idBuff = new StringBuilder(idNoDashes);
|
|
||||||
idBuff.insert(20, '-');
|
|
||||||
idBuff.insert(16, '-');
|
|
||||||
idBuff.insert(12, '-');
|
|
||||||
idBuff.insert(8, '-');
|
|
||||||
return idBuff.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package cc.fascinated.common;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.web.client.HttpClientErrorException;
|
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class WebRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The web client.
|
|
||||||
*/
|
|
||||||
private static final RestClient CLIENT = RestClient.builder()
|
|
||||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a response from the given URL.
|
|
||||||
*
|
|
||||||
* @param url the url
|
|
||||||
* @return the response
|
|
||||||
* @param <T> the type of the response
|
|
||||||
*/
|
|
||||||
public static <T> T getAsEntity(String url, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
ResponseEntity<T> profile = CLIENT.get()
|
|
||||||
.uri(url)
|
|
||||||
.retrieve()
|
|
||||||
.toEntity(clazz);
|
|
||||||
|
|
||||||
if (profile.getStatusCode().isError()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return profile.getBody();
|
|
||||||
} catch (HttpClientErrorException ex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package cc.fascinated.config;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Getter
|
|
||||||
public class Config {
|
|
||||||
public static Config INSTANCE;
|
|
||||||
|
|
||||||
@Value("${public-url}")
|
|
||||||
private String webPublicUrl;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void onInitialize() {
|
|
||||||
INSTANCE = this;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package cc.fascinated.controller;
|
|
||||||
|
|
||||||
import cc.fascinated.config.Config;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
@RequestMapping(value = "/")
|
|
||||||
public class HomeController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The example UUID.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("FieldCanBeLocal")
|
|
||||||
private final String exampleUuid = "eeab5f8a-18dd-4d58-af78-2b3c4543da48";
|
|
||||||
|
|
||||||
@RequestMapping(value = "/")
|
|
||||||
public String home(Model model) {
|
|
||||||
model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid);
|
|
||||||
return "index";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package cc.fascinated.controller;
|
|
||||||
|
|
||||||
import cc.fascinated.common.PlayerUtils;
|
|
||||||
import cc.fascinated.model.player.Player;
|
|
||||||
import cc.fascinated.model.player.Skin;
|
|
||||||
import cc.fascinated.model.response.impl.InvalidPartResponse;
|
|
||||||
import cc.fascinated.model.response.impl.PlayerNotFoundResponse;
|
|
||||||
import cc.fascinated.service.PlayerService;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.CacheControl;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping(value = "/player/")
|
|
||||||
public class PlayerController {
|
|
||||||
|
|
||||||
private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic();
|
|
||||||
private final PlayerService playerManagerService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public PlayerController(PlayerService playerManagerService) {
|
|
||||||
this.playerManagerService = playerManagerService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ResponseBody
|
|
||||||
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
||||||
public ResponseEntity<?> getPlayer(@PathVariable String id) {
|
|
||||||
Player player = playerManagerService.getPlayer(id);
|
|
||||||
if (player == null) { // No player with that id was found
|
|
||||||
return new PlayerNotFoundResponse().toResponseEntity();
|
|
||||||
}
|
|
||||||
// Return the player
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.cacheControl(cacheControl)
|
|
||||||
.body(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(value = "/{part}/{id}")
|
|
||||||
public ResponseEntity<?> getPlayerHead(@PathVariable String part,
|
|
||||||
@PathVariable String id,
|
|
||||||
@RequestParam(required = false, defaultValue = "256") int size) {
|
|
||||||
Player player = playerManagerService.getPlayer(id);
|
|
||||||
byte[] partBytes = new byte[0];
|
|
||||||
if (player != null) { // The player exists
|
|
||||||
Skin skin = player.getSkin();
|
|
||||||
Skin.Parts skinPart = Skin.Parts.fromName(part);
|
|
||||||
if (skinPart == null) { // Unknown part name
|
|
||||||
return new InvalidPartResponse().toResponseEntity();
|
|
||||||
}
|
|
||||||
partBytes = PlayerUtils.getSkinPartBytes(skin, skinPart, size);
|
|
||||||
}
|
|
||||||
if (partBytes == null) { // Fallback to the default head
|
|
||||||
partBytes = PlayerUtils.getSkinPartBytes(Skin.DEFAULT_SKIN, Skin.Parts.HEAD, size);
|
|
||||||
}
|
|
||||||
// Return the part image
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.cacheControl(cacheControl)
|
|
||||||
.contentType(MediaType.IMAGE_PNG)
|
|
||||||
.body(partBytes);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package cc.fascinated.controller;
|
|
||||||
|
|
||||||
import cc.fascinated.model.server.MinecraftServer;
|
|
||||||
import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping(value = "/server/")
|
|
||||||
public class ServerController {
|
|
||||||
|
|
||||||
@ResponseBody
|
|
||||||
@GetMapping(value = "/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
||||||
public ResponseEntity<MinecraftServer> getServer(@PathVariable String hostname) {
|
|
||||||
return ResponseEntity.ok(JavaMinecraftServerPinger.INSTANCE.ping(hostname, 25565));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package cc.fascinated.model.mojang;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @ToString
|
|
||||||
public final class JavaServerStatusToken {
|
|
||||||
private final String description;
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package cc.fascinated.model.player;
|
|
||||||
|
|
||||||
import cc.fascinated.common.Tuple;
|
|
||||||
import cc.fascinated.common.UUIDUtils;
|
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class Player {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The UUID of the player
|
|
||||||
*/
|
|
||||||
private final UUID uuid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The username of the player
|
|
||||||
*/
|
|
||||||
@JsonProperty("username")
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The skin of the player, null if the
|
|
||||||
* player does not have a skin
|
|
||||||
*/
|
|
||||||
private Skin skin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cape of the player, null if the
|
|
||||||
* player does not have a cape
|
|
||||||
*/
|
|
||||||
private Cape cape;
|
|
||||||
|
|
||||||
public Player(MojangProfile profile) {
|
|
||||||
this.uuid = UUID.fromString(UUIDUtils.addUuidDashes(profile.getId()));
|
|
||||||
this.name = profile.getName();
|
|
||||||
|
|
||||||
// Get the skin and cape
|
|
||||||
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();
|
|
||||||
if (skinAndCape != null) {
|
|
||||||
this.skin = skinAndCape.getLeft();
|
|
||||||
this.cape = skinAndCape.getRight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
package cc.fascinated.model.player;
|
|
||||||
|
|
||||||
import cc.fascinated.common.PlayerUtils;
|
|
||||||
import cc.fascinated.config.Config;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Getter @Log4j2
|
|
||||||
public class Skin {
|
|
||||||
/**
|
|
||||||
* The default skin, usually used when the skin is not found.
|
|
||||||
*/
|
|
||||||
public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1",
|
|
||||||
Model.DEFAULT);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL for the skin
|
|
||||||
*/
|
|
||||||
private final String url;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The model for the skin
|
|
||||||
*/
|
|
||||||
private final Model model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The skin image for the skin
|
|
||||||
*/
|
|
||||||
@JsonIgnore
|
|
||||||
private final BufferedImage skinImage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The part URLs of the skin
|
|
||||||
*/
|
|
||||||
@JsonProperty("parts")
|
|
||||||
private final Map<String, String> partUrls = new HashMap<>();
|
|
||||||
|
|
||||||
public Skin(String url, Model model) {
|
|
||||||
this.url = url;
|
|
||||||
this.model = model;
|
|
||||||
|
|
||||||
this.skinImage = PlayerUtils.getSkinImage(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the skin from a {@link JsonObject}.
|
|
||||||
*
|
|
||||||
* @param json the JSON object
|
|
||||||
* @return the skin
|
|
||||||
*/
|
|
||||||
public static Skin fromJson(JsonObject json) {
|
|
||||||
if (json == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String url = json.get("url").getAsString();
|
|
||||||
JsonObject metadata = json.getAsJsonObject("metadata");
|
|
||||||
Model model = Model.fromName(metadata == null ? "slim" : // Fall back to slim if the model is not found
|
|
||||||
metadata.get("model").getAsString());
|
|
||||||
return new Skin(url, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates the part URLs for the skin.
|
|
||||||
*
|
|
||||||
* @param playerUuid the player's UUID
|
|
||||||
*/
|
|
||||||
public Skin populatePartUrls(String playerUuid) {
|
|
||||||
for (Parts part : Parts.values()) {
|
|
||||||
String partName = part.name().toLowerCase();
|
|
||||||
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=" + part.getDefaultSize());
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The skin part enum that contains the
|
|
||||||
* information about the part.
|
|
||||||
*/
|
|
||||||
@Getter @AllArgsConstructor
|
|
||||||
public enum Parts {
|
|
||||||
|
|
||||||
HEAD(8, 8, 8, 8, 256);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The x and y position of the part.
|
|
||||||
*/
|
|
||||||
private final int x, y;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The width and height of the part.
|
|
||||||
*/
|
|
||||||
private final int width, height;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scale of the part.
|
|
||||||
*/
|
|
||||||
private final int defaultSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the name of the part.
|
|
||||||
*
|
|
||||||
* @return the name of the part
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
return this.name().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the skin part from its name.
|
|
||||||
*
|
|
||||||
* @param name the name of the part
|
|
||||||
* @return the skin part
|
|
||||||
*/
|
|
||||||
public static Parts fromName(String name) {
|
|
||||||
for (Parts part : values()) {
|
|
||||||
if (part.name().equalsIgnoreCase(name)) {
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The model of the skin.
|
|
||||||
*/
|
|
||||||
public enum Model {
|
|
||||||
DEFAULT,
|
|
||||||
SLIM;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the model from its name.
|
|
||||||
*
|
|
||||||
* @param name the name of the model
|
|
||||||
* @return the model
|
|
||||||
*/
|
|
||||||
public static Model fromName(String name) {
|
|
||||||
for (Model model : values()) {
|
|
||||||
if (model.name().equalsIgnoreCase(name)) {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package cc.fascinated.model.response;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
|
|
||||||
@Getter @AllArgsConstructor
|
|
||||||
public class Response {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The status code of this error.
|
|
||||||
*/
|
|
||||||
private HttpStatus status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The message of this error.
|
|
||||||
*/
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets this response as a {@link ResponseEntity}.
|
|
||||||
*
|
|
||||||
* @return the response entity
|
|
||||||
*/
|
|
||||||
public ResponseEntity<?> toResponseEntity() {
|
|
||||||
return new ResponseEntity<>(this, status);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package cc.fascinated.model.response.impl;
|
|
||||||
|
|
||||||
import cc.fascinated.model.response.Response;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class InvalidPartResponse extends Response {
|
|
||||||
|
|
||||||
public InvalidPartResponse() {
|
|
||||||
super(HttpStatus.NOT_FOUND, "Invalid part name.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package cc.fascinated.model.response.impl;
|
|
||||||
|
|
||||||
import cc.fascinated.model.response.Response;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
|
|
||||||
public class PlayerNotFoundResponse extends Response {
|
|
||||||
|
|
||||||
public PlayerNotFoundResponse() {
|
|
||||||
super(HttpStatus.NOT_FOUND, "Player not found.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package cc.fascinated.model.server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
public final class JavaMinecraftServer extends MinecraftServer {
|
|
||||||
public JavaMinecraftServer(String hostname, int port, String motd) {
|
|
||||||
super(hostname, port, motd);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package cc.fascinated.model.server;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor @Getter @ToString
|
|
||||||
public class MinecraftServer {
|
|
||||||
private final String hostname;
|
|
||||||
private final int port;
|
|
||||||
private final String motd;
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package cc.fascinated.service;
|
|
||||||
|
|
||||||
import cc.fascinated.common.WebRequest;
|
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
|
||||||
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service @Log4j2
|
|
||||||
public class MojangAPIService {
|
|
||||||
|
|
||||||
@Value("${mojang.session-server}")
|
|
||||||
private String mojangSessionServerUrl;
|
|
||||||
|
|
||||||
@Value("${mojang.api}")
|
|
||||||
private String mojangApiUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Session Server profile of the
|
|
||||||
* player with the given UUID.
|
|
||||||
*
|
|
||||||
* @param id the uuid or name of the player
|
|
||||||
* @return the profile
|
|
||||||
*/
|
|
||||||
public MojangProfile getProfile(String id) {
|
|
||||||
return WebRequest.getAsEntity(mojangSessionServerUrl + "/session/minecraft/profile/" + id, MojangProfile.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the UUID of the player using
|
|
||||||
* the name of the player.
|
|
||||||
*
|
|
||||||
* @param id the name of the player
|
|
||||||
* @return the profile
|
|
||||||
*/
|
|
||||||
public MojangUsernameToUuid getUuidFromUsername(String id) {
|
|
||||||
return WebRequest.getAsEntity(mojangApiUrl + "/users/profiles/minecraft/" + id, MojangUsernameToUuid.class);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package cc.fascinated.service;
|
|
||||||
|
|
||||||
import cc.fascinated.common.UUIDUtils;
|
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
|
||||||
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
|
||||||
import cc.fascinated.model.player.Player;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import net.jodah.expiringmap.ExpirationPolicy;
|
|
||||||
import net.jodah.expiringmap.ExpiringMap;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@Service @Log4j2
|
|
||||||
public class PlayerService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cache of players.
|
|
||||||
*/
|
|
||||||
private final Map<UUID, Player> players = ExpiringMap.builder()
|
|
||||||
.expiration(1, TimeUnit.HOURS)
|
|
||||||
.expirationPolicy(ExpirationPolicy.CREATED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cache of player names to UUIDs.
|
|
||||||
*/
|
|
||||||
private final Map<String, UUID> playerNameToUUIDCache = ExpiringMap.builder()
|
|
||||||
.expiration(1, TimeUnit.DAYS)
|
|
||||||
.expirationPolicy(ExpirationPolicy.CREATED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private final MojangAPIService mojangAPIService;
|
|
||||||
|
|
||||||
public PlayerService(MojangAPIService mojangAPIService) {
|
|
||||||
this.mojangAPIService = mojangAPIService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a player by their UUID.
|
|
||||||
*
|
|
||||||
* @param id the uuid or name of the player
|
|
||||||
* @return the player or null if the player does not exist
|
|
||||||
*/
|
|
||||||
public Player getPlayer(String id) {
|
|
||||||
UUID uuid = null;
|
|
||||||
if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID
|
|
||||||
try {
|
|
||||||
uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
} else { // Check if the id is a name
|
|
||||||
uuid = playerNameToUUIDCache.get(id.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the player is cached
|
|
||||||
if (uuid != null && players.containsKey(uuid)) {
|
|
||||||
return players.get(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
MojangProfile profile = uuid == null ? null : mojangAPIService.getProfile(uuid.toString());
|
|
||||||
if (profile == null) { // The player cannot be found using their UUID
|
|
||||||
MojangUsernameToUuid apiProfile = mojangAPIService.getUuidFromUsername(id); // Get the UUID of the player using their name
|
|
||||||
if (apiProfile == null || !apiProfile.isValid()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Get the profile of the player using their UUID
|
|
||||||
profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ?
|
|
||||||
UUIDUtils.addUuidDashes(apiProfile.getId()) : apiProfile.getId());
|
|
||||||
}
|
|
||||||
if (profile == null) { // The player cannot be found using their name or UUID
|
|
||||||
log.info("Player with id {} could not be found", id);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Player player = new Player(profile);
|
|
||||||
players.put(player.getUuid(), player);
|
|
||||||
playerNameToUUIDCache.put(player.getName().toUpperCase(), player.getUuid());
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package cc.fascinated.service.pinger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
* @param <T> the type of server to ping
|
|
||||||
*/
|
|
||||||
public interface MinecraftServerPinger<T> {
|
|
||||||
T ping(String hostname, int port);
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package cc.fascinated.service.pinger.impl;
|
|
||||||
|
|
||||||
import cc.fascinated.Main;
|
|
||||||
import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
|
|
||||||
import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart;
|
|
||||||
import cc.fascinated.model.mojang.JavaServerStatusToken;
|
|
||||||
import cc.fascinated.model.server.JavaMinecraftServer;
|
|
||||||
import cc.fascinated.service.pinger.MinecraftServerPinger;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
*/
|
|
||||||
@Log4j2(topic = "Java Pinger")
|
|
||||||
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
|
|
||||||
public static final JavaMinecraftServerPinger INSTANCE = new JavaMinecraftServerPinger();
|
|
||||||
|
|
||||||
private static final int TIMEOUT = 3000; // The timeout for the socket
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JavaMinecraftServer ping(String hostname, int port) {
|
|
||||||
log.info("Pinging {}:{}...", hostname, port);
|
|
||||||
|
|
||||||
// Open a socket connection to the server
|
|
||||||
try (Socket socket = new Socket()) {
|
|
||||||
socket.setTcpNoDelay(true);
|
|
||||||
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
|
|
||||||
|
|
||||||
// Open data streams to begin packet transaction
|
|
||||||
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
|
|
||||||
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
|
|
||||||
// Begin handshaking with the server
|
|
||||||
new JavaPacketHandshakingInSetProtocol(hostname, port, 47).process(inputStream, outputStream);
|
|
||||||
|
|
||||||
// Send the status request to the server, and await back the response
|
|
||||||
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
|
||||||
packetStatusInStart.process(inputStream, outputStream);
|
|
||||||
System.out.println("packetStatusInStart.getResponse() = " + packetStatusInStart.getResponse());
|
|
||||||
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
|
|
||||||
return new JavaMinecraftServer(hostname, port, token.getDescription());
|
|
||||||
}
|
|
||||||
} catch (IOException ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package cc.fascinated;
|
package xyz.mcutils.backend;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
@ -12,10 +13,12 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@SpringBootApplication @Log4j2
|
@Log4j2(topic = "Main")
|
||||||
|
@SpringBootApplication
|
||||||
public class Main {
|
public class Main {
|
||||||
|
public static final Gson GSON = new GsonBuilder()
|
||||||
public static final Gson GSON = new Gson();
|
.setDateFormat("MM-dd-yyyy HH:mm:ss")
|
||||||
|
.create();
|
||||||
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
|
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@ -30,6 +33,6 @@ public class Main {
|
|||||||
}
|
}
|
||||||
log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
|
log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config
|
||||||
|
|
||||||
SpringApplication.run(Main.class, args);
|
SpringApplication.run(Main.class, args); // Start the application
|
||||||
}
|
}
|
||||||
}
|
}
|
30
src/main/java/xyz/mcutils/backend/common/AppConfig.java
Normal file
30
src/main/java/xyz/mcutils/backend/common/AppConfig.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public final class AppConfig {
|
||||||
|
/**
|
||||||
|
* Is the app running in a production environment?
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private static final boolean production;
|
||||||
|
static { // Are we running on production?
|
||||||
|
String env = System.getenv("ENVIRONMENT");
|
||||||
|
production = env != null && (env.equals("production"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the app running in a test environment?
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private static boolean isRunningTest = true;
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
isRunningTest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/main/java/xyz/mcutils/backend/common/CachedResponse.java
Normal file
59
src/main/java/xyz/mcutils/backend/common/CachedResponse.java
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@AllArgsConstructor @NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class CachedResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cache information for this response.
|
||||||
|
*/
|
||||||
|
private Cache cache;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter @Setter
|
||||||
|
public static class Cache {
|
||||||
|
/**
|
||||||
|
* Whether this request is cached.
|
||||||
|
*/
|
||||||
|
private boolean cached;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unix timestamp of when this was cached.
|
||||||
|
*/
|
||||||
|
private long cachedTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new cache information object with the default values.
|
||||||
|
* <p>
|
||||||
|
* The default values are:
|
||||||
|
* <br>
|
||||||
|
* <ul>
|
||||||
|
* <li>cached: true</li>
|
||||||
|
* <li>cachedAt: {@link System#currentTimeMillis()}</li>
|
||||||
|
* </ul>
|
||||||
|
* <br>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the default cache information object
|
||||||
|
*/
|
||||||
|
public static Cache defaultCache() {
|
||||||
|
return new Cache(true, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets if this request is cached.
|
||||||
|
*
|
||||||
|
* @param cached the new value of if this request is cached
|
||||||
|
*/
|
||||||
|
public void setCached(boolean cached) {
|
||||||
|
this.cached = cached;
|
||||||
|
if (!cached) {
|
||||||
|
cachedTime = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
src/main/java/xyz/mcutils/backend/common/ColorUtils.java
Normal file
112
src/main/java/xyz/mcutils/backend/common/ColorUtils.java
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public final class ColorUtils {
|
||||||
|
private static final Pattern STRIP_COLOR_PATTERN = Pattern.compile("(?i)§[0-9A-FK-OR]");
|
||||||
|
private static final Map<Character, String> COLOR_MAP = new HashMap<>();
|
||||||
|
static {
|
||||||
|
// Map each color to its corresponding hex code
|
||||||
|
COLOR_MAP.put('0', "#000000"); // Black
|
||||||
|
COLOR_MAP.put('1', "#0000AA"); // Dark Blue
|
||||||
|
COLOR_MAP.put('2', "#00AA00"); // Dark Green
|
||||||
|
COLOR_MAP.put('3', "#00AAAA"); // Dark Aqua
|
||||||
|
COLOR_MAP.put('4', "#AA0000"); // Dark Red
|
||||||
|
COLOR_MAP.put('5', "#AA00AA"); // Dark Purple
|
||||||
|
COLOR_MAP.put('6', "#FFAA00"); // Gold
|
||||||
|
COLOR_MAP.put('7', "#AAAAAA"); // Gray
|
||||||
|
COLOR_MAP.put('8', "#555555"); // Dark Gray
|
||||||
|
COLOR_MAP.put('9', "#5555FF"); // Blue
|
||||||
|
COLOR_MAP.put('a', "#55FF55"); // Green
|
||||||
|
COLOR_MAP.put('b', "#55FFFF"); // Aqua
|
||||||
|
COLOR_MAP.put('c', "#FF5555"); // Red
|
||||||
|
COLOR_MAP.put('d', "#FF55FF"); // Light Purple
|
||||||
|
COLOR_MAP.put('e', "#FFFF55"); // Yellow
|
||||||
|
COLOR_MAP.put('f', "#FFFFFF"); // White
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the color codes
|
||||||
|
* from the given input.
|
||||||
|
*
|
||||||
|
* @param input the input to strip
|
||||||
|
* @return the stripped input
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String stripColor(@NonNull String input) {
|
||||||
|
return STRIP_COLOR_PATTERN.matcher(input).replaceAll("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given input
|
||||||
|
* into HTML.
|
||||||
|
*
|
||||||
|
* @param input the input to convert
|
||||||
|
* @return the HTML converted input
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String toHTML(@NonNull String input) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
boolean nextIsColor = false; // Is the next char a color code?
|
||||||
|
|
||||||
|
// Get the leading spaces from the first line
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
boolean foundNonSpace = false;
|
||||||
|
for (char character : input.toCharArray()) {
|
||||||
|
if (character == ' ' && !foundNonSpace) {
|
||||||
|
leadingSpaces++;
|
||||||
|
} else {
|
||||||
|
foundNonSpace = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char character : input.toCharArray()) {
|
||||||
|
// Found color symbol, next color is the color
|
||||||
|
if (character == '§') {
|
||||||
|
nextIsColor = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nextIsColor) { // Map the current color to its hex code
|
||||||
|
String color = COLOR_MAP.getOrDefault(Character.toLowerCase(character), "");
|
||||||
|
builder.append("<span style=\"color:").append(color).append("\">");
|
||||||
|
nextIsColor = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character == ' ') { // Preserve space character
|
||||||
|
builder.append(" ");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
builder.append(character); // Append the char...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add leading spaces to the end of the HTML string
|
||||||
|
builder.append(" ".repeat(Math.max(0, leadingSpaces)));
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link Color} from a Minecraft color code.
|
||||||
|
*
|
||||||
|
* @param colorCode the color code to get the color from
|
||||||
|
* @return the color
|
||||||
|
*/
|
||||||
|
public static Color getMinecraftColor(char colorCode) {
|
||||||
|
String color = COLOR_MAP.getOrDefault(colorCode, null);
|
||||||
|
if (color == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid color code: " + colorCode);
|
||||||
|
}
|
||||||
|
return Color.decode(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
src/main/java/xyz/mcutils/backend/common/DNSUtils.java
Normal file
58
src/main/java/xyz/mcutils/backend/common/DNSUtils.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import org.xbill.DNS.Lookup;
|
||||||
|
import org.xbill.DNS.Record;
|
||||||
|
import org.xbill.DNS.Type;
|
||||||
|
import xyz.mcutils.backend.model.dns.impl.ARecord;
|
||||||
|
import xyz.mcutils.backend.model.dns.impl.SRVRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public final class DNSUtils {
|
||||||
|
private static final String SRV_QUERY_PREFIX = "_minecraft._tcp.%s";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resolved address and port of the
|
||||||
|
* given hostname by resolving the SRV records.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname to resolve
|
||||||
|
* @return the resolved address and port, null if none
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static SRVRecord resolveSRV(@NonNull String hostname) {
|
||||||
|
Record[] records = new Lookup(SRV_QUERY_PREFIX.formatted(hostname), Type.SRV).run(); // Resolve SRV records
|
||||||
|
if (records == null) { // No records exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SRVRecord result = null;
|
||||||
|
for (Record record : records) {
|
||||||
|
result = new SRVRecord((org.xbill.DNS.SRVRecord) record);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resolved address of the given
|
||||||
|
* hostname by resolving the A records.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname to resolve
|
||||||
|
* @return the resolved address, null if none
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static ARecord resolveA(@NonNull String hostname) {
|
||||||
|
Record[] records = new Lookup(hostname, Type.A).run(); // Resolve A records
|
||||||
|
if (records == null) { // No records exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ARecord result = null;
|
||||||
|
for (Record record : records) {
|
||||||
|
result = new ARecord((org.xbill.DNS.ARecord) record);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
21
src/main/java/xyz/mcutils/backend/common/Endpoint.java
Normal file
21
src/main/java/xyz/mcutils/backend/common/Endpoint.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public class Endpoint {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The endpoint.
|
||||||
|
*/
|
||||||
|
private final String endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The statuses that indicate that the endpoint is online.
|
||||||
|
*/
|
||||||
|
private final List<HttpStatusCode> allowedStatuses;
|
||||||
|
}
|
26
src/main/java/xyz/mcutils/backend/common/EnumUtils.java
Normal file
26
src/main/java/xyz/mcutils/backend/common/EnumUtils.java
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@UtilityClass
|
||||||
|
public final class EnumUtils {
|
||||||
|
/**
|
||||||
|
* Get the enum constant of the specified enum type with the specified name.
|
||||||
|
*
|
||||||
|
* @param enumType the enum type
|
||||||
|
* @param name the name of the constant to return
|
||||||
|
* @param <T> the type of the enum
|
||||||
|
* @return the enum constant of the specified enum type with the specified name
|
||||||
|
*/
|
||||||
|
public <T extends Enum<T>> T getEnumConstant(@NonNull Class<T> enumType, @NonNull String name) {
|
||||||
|
try {
|
||||||
|
return Enum.valueOf(enumType, name);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
src/main/java/xyz/mcutils/backend/common/ExpiringSet.java
Normal file
132
src/main/java/xyz/mcutils/backend/common/ExpiringSet.java
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import net.jodah.expiringmap.ExpirationPolicy;
|
||||||
|
import net.jodah.expiringmap.ExpiringMap;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple set that expires elements after a certain
|
||||||
|
* amount of time, utilizing the {@link ExpiringMap} library.
|
||||||
|
*
|
||||||
|
* @param <T> The type of element to store within this set
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public final class ExpiringSet<T> implements Iterable<T> {
|
||||||
|
/**
|
||||||
|
* The internal cache for this set.
|
||||||
|
*/
|
||||||
|
@NonNull private final ExpiringMap<T, Long> cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lifetime (in millis) of the elements in this set.
|
||||||
|
*/
|
||||||
|
private final long lifetime;
|
||||||
|
|
||||||
|
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit) {
|
||||||
|
this(expirationPolicy, duration, timeUnit, ignored -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit, @NonNull Consumer<T> onExpire) {
|
||||||
|
//noinspection unchecked
|
||||||
|
this.cache = ExpiringMap.builder()
|
||||||
|
.expirationPolicy(expirationPolicy)
|
||||||
|
.expiration(duration, timeUnit)
|
||||||
|
.expirationListener((key, ignored) -> onExpire.accept((T) key))
|
||||||
|
.build();
|
||||||
|
this.lifetime = timeUnit.toMillis(duration); // Get the lifetime in millis
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an element to this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element was added
|
||||||
|
*/
|
||||||
|
public boolean add(@NonNull T element) {
|
||||||
|
boolean contains = contains(element); // Does this set already contain the element?
|
||||||
|
this.cache.put(element, System.currentTimeMillis() + this.lifetime);
|
||||||
|
return !contains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entry time of an element in this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return the entry time, -1 if not contained
|
||||||
|
*/
|
||||||
|
public long getEntryTime(@NonNull T element) {
|
||||||
|
return contains(element) ? this.cache.get(element) - this.lifetime : -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element is
|
||||||
|
* contained within this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element is contained
|
||||||
|
*/
|
||||||
|
public boolean contains(@NonNull T element) {
|
||||||
|
Long timeout = this.cache.get(element); // Get the timeout for the element
|
||||||
|
return timeout != null && (timeout > System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this set is empty.
|
||||||
|
*
|
||||||
|
* @return whether this set is empty
|
||||||
|
*/
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.cache.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of this set.
|
||||||
|
*
|
||||||
|
* @return the size
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return this.cache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an element from this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element was removed
|
||||||
|
*/
|
||||||
|
public boolean remove(@NonNull T element) {
|
||||||
|
return this.cache.remove(element) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear this set.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the elements in this set.
|
||||||
|
*
|
||||||
|
* @return the elements
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Set<T> getElements() {
|
||||||
|
return this.cache.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator over elements of type {@code T}.
|
||||||
|
*
|
||||||
|
* @return an Iterator.
|
||||||
|
*/
|
||||||
|
@Override @NonNull
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return this.cache.keySet().iterator();
|
||||||
|
}
|
||||||
|
}
|
28
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal file
28
src/main/java/xyz/mcutils/backend/common/Fonts.java
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.Main;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
@Log4j2(topic = "Fonts")
|
||||||
|
public class Fonts {
|
||||||
|
|
||||||
|
public static final Font MINECRAFT;
|
||||||
|
public static final Font MINECRAFT_BOLD;
|
||||||
|
public static final Font MINECRAFT_ITALIC;
|
||||||
|
|
||||||
|
static {
|
||||||
|
InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
|
||||||
|
try {
|
||||||
|
MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
|
||||||
|
MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
|
||||||
|
MINECRAFT_ITALIC = MINECRAFT.deriveFont(Font.ITALIC);
|
||||||
|
} catch (FontFormatException | IOException e) {
|
||||||
|
log.error("Failed to load Minecraft font", e);
|
||||||
|
throw new RuntimeException("Failed to load Minecraft font", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.common;
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
78
src/main/java/xyz/mcutils/backend/common/ImageUtils.java
Normal file
78
src/main/java/xyz/mcutils/backend/common/ImageUtils.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
@Log4j2(topic = "Image Utils")
|
||||||
|
public class ImageUtils {
|
||||||
|
/**
|
||||||
|
* Scale the given image to the provided scale.
|
||||||
|
*
|
||||||
|
* @param image the image to scale
|
||||||
|
* @param scale the scale to scale the image to
|
||||||
|
* @return the scaled image
|
||||||
|
*/
|
||||||
|
public static BufferedImage resize(BufferedImage image, double scale) {
|
||||||
|
BufferedImage scaled = new BufferedImage((int) (image.getWidth() * scale), (int) (image.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = scaled.createGraphics();
|
||||||
|
graphics.drawImage(image, AffineTransform.getScaleInstance(scale, scale), null);
|
||||||
|
graphics.dispose();
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flip the given image.
|
||||||
|
*
|
||||||
|
* @param image the image to flip
|
||||||
|
* @return the flipped image
|
||||||
|
*/
|
||||||
|
public static BufferedImage flip(@NotNull final BufferedImage image) {
|
||||||
|
BufferedImage flipped = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = flipped.createGraphics();
|
||||||
|
graphics.drawImage(image, image.getWidth(), 0, 0, image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), null);
|
||||||
|
graphics.dispose();
|
||||||
|
return flipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an image to bytes.
|
||||||
|
*
|
||||||
|
* @param image the image to convert
|
||||||
|
* @return the image as bytes
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static byte[] imageToBytes(BufferedImage image) {
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(image, "png", outputStream);
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new Exception("Failed to convert image to bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a base64 string to an image.
|
||||||
|
*
|
||||||
|
* @param base64 the base64 string to convert
|
||||||
|
* @return the image
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static BufferedImage base64ToImage(String base64) {
|
||||||
|
String favicon = base64.contains("data:image/png;base64,") ? base64.split(",")[1] : base64;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(favicon)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new Exception("Failed to convert base64 to image", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Protocol_version_numbers">Protocol Version Numbers</a>
|
||||||
|
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-16">Spigot NMS (1.16+)</a>
|
||||||
|
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-1-10-1-15">Spigot NMS (1.10 - 1.15)</a>
|
||||||
|
* @see <a href="https://www.spigotmc.org/wiki/spigot-nms-and-minecraft-versions-legacy">Spigot NMS (1.8 - 1.9)</a>
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor @Getter @ToString @Log4j2(topic = "Minecraft Version")
|
||||||
|
public enum JavaMinecraftVersion {
|
||||||
|
V1_20_3(765, "v1_20_R3"), // 1.20.3 & 1.20.4
|
||||||
|
V1_20_2(764, "v1_20_R2"), // 1.20.2
|
||||||
|
V1_20(763, "v1_20_R1"), // 1.20 & 1.20.1
|
||||||
|
|
||||||
|
V1_19_4(762, "v1_19_R3"), // 1.19.4
|
||||||
|
V1_19_3(761, "v1_19_R2"), // 1.19.3
|
||||||
|
V1_19_1(760, "v1_19_R1"), // 1.19.1 & 1.19.2
|
||||||
|
V1_19(759, "v1_19_R1"), // 1.19
|
||||||
|
|
||||||
|
V1_18_2(758, "v1_18_R2"), // 1.18.2
|
||||||
|
V1_18(757, "v1_18_R1"), // 1.18 & 1.18.1
|
||||||
|
|
||||||
|
V1_17_1(756, "v1_17_R1"), // 1.17.1
|
||||||
|
V1_17(755, "v1_17_R1"), // 1.17
|
||||||
|
|
||||||
|
V1_16_4(754, "v1_16_R3"), // 1.16.4 & 1.16.5
|
||||||
|
V1_16_3(753, "v1_16_R2"), // 1.16.3
|
||||||
|
V1_16_2(751, "v1_16_R2"), // 1.16.2
|
||||||
|
V1_16_1(736, "v1_16_R1"), // 1.16.1
|
||||||
|
V1_16(735, "v1_16_R1"), // 1.16
|
||||||
|
|
||||||
|
V1_15_2(578, "v1_15_R1"), // 1.15.2
|
||||||
|
V1_15_1(575, "v1_15_R1"), // 1.15.1
|
||||||
|
V1_15(573, "v1_15_R1"), // 1.15
|
||||||
|
|
||||||
|
V1_14_4(498, "v1_14_R1"), // 1.14.4
|
||||||
|
V1_14_3(490, "v1_14_R1"), // 1.14.3
|
||||||
|
V1_14_2(485, "v1_14_R1"), // 1.14.2
|
||||||
|
V1_14_1(480, "v1_14_R1"), // 1.14.1
|
||||||
|
V1_14(477, "v1_14_R1"), // 1.14
|
||||||
|
|
||||||
|
V1_13_2(404, "v1_13_R2"), // 1.13.2
|
||||||
|
V1_13_1(401, "v1_13_R2"), // 1.13.1
|
||||||
|
V1_13(393, "v1_13_R1"), // 1.13
|
||||||
|
|
||||||
|
V1_12_2(340, "v1_12_R1"), // 1.12.2
|
||||||
|
V1_12_1(338, "v1_12_R1"), // 1.12.1
|
||||||
|
V1_12(335, "v1_12_R1"), // 1.12
|
||||||
|
|
||||||
|
V1_11_1(316, "v1_11_R1"), // 1.11.1 & 1.11.2
|
||||||
|
V1_11(315, "v1_11_R1"), // 1.11
|
||||||
|
|
||||||
|
V1_10(210, "v1_10_R1"), // 1.10.x
|
||||||
|
|
||||||
|
V1_9_3(110, "v1_9_R2"), // 1.9.3 & 1.9.4
|
||||||
|
V1_9_2(109, "v1_9_R1"), // 1.9.2
|
||||||
|
V1_9_1(108, "v1_9_R1"), // 1.9.1
|
||||||
|
V1_9(107, "v1_9_R1"), // 1.9
|
||||||
|
|
||||||
|
V1_8(47, "v1_8_R3"), // 1.8.x
|
||||||
|
|
||||||
|
V1_7_6(5, "v1_7_R4"), // 1.7.6 - 1.7.10
|
||||||
|
|
||||||
|
UNKNOWN(-1, "Unknown");
|
||||||
|
|
||||||
|
// Game Updates
|
||||||
|
public static final JavaMinecraftVersion TRAILS_AND_TALES = JavaMinecraftVersion.V1_20;
|
||||||
|
public static final JavaMinecraftVersion THE_WILD_UPDATE = JavaMinecraftVersion.V1_19;
|
||||||
|
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_2 = JavaMinecraftVersion.V1_18;
|
||||||
|
public static final JavaMinecraftVersion CAVES_AND_CLIFFS_PT_1 = JavaMinecraftVersion.V1_17;
|
||||||
|
public static final JavaMinecraftVersion NETHER_UPDATE = JavaMinecraftVersion.V1_16;
|
||||||
|
public static final JavaMinecraftVersion BUZZY_BEES = JavaMinecraftVersion.V1_15;
|
||||||
|
public static final JavaMinecraftVersion VILLAGE_AND_PILLAGE = JavaMinecraftVersion.V1_14;
|
||||||
|
public static final JavaMinecraftVersion UPDATE_AQUATIC = JavaMinecraftVersion.V1_13;
|
||||||
|
public static final JavaMinecraftVersion WORLD_OF_COLOR_UPDATE = JavaMinecraftVersion.V1_12;
|
||||||
|
public static final JavaMinecraftVersion EXPLORATION_UPDATE = JavaMinecraftVersion.V1_11;
|
||||||
|
public static final JavaMinecraftVersion FROSTBURN_UPDATE = JavaMinecraftVersion.V1_10;
|
||||||
|
public static final JavaMinecraftVersion THE_COMBAT_UPDATE = JavaMinecraftVersion.V1_9;
|
||||||
|
public static final JavaMinecraftVersion BOUNTIFUL_UPDATE = JavaMinecraftVersion.V1_8;
|
||||||
|
|
||||||
|
private static final JavaMinecraftVersion[] VALUES = JavaMinecraftVersion.values();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The protocol number of this version.
|
||||||
|
*/
|
||||||
|
private final int protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server version for this version.
|
||||||
|
*/
|
||||||
|
private final String nmsVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cached name of this version.
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of this protocol version.
|
||||||
|
*
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
// We have a name
|
||||||
|
if (this.name != null) {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
// Use the server version as the name if unknown
|
||||||
|
if (this == UNKNOWN) {
|
||||||
|
this.name = this.getNmsVersion();
|
||||||
|
} else { // Parse the name
|
||||||
|
this.name = name().substring(1);
|
||||||
|
this.name = this.name.replace("_", ".");
|
||||||
|
}
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this version legacy?
|
||||||
|
*
|
||||||
|
* @return whether this version is legacy
|
||||||
|
*/
|
||||||
|
public boolean isLegacy() {
|
||||||
|
return this.isBelow(JavaMinecraftVersion.V1_16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this version is
|
||||||
|
* above the one given.
|
||||||
|
*
|
||||||
|
* @param other the other version
|
||||||
|
* @return true if above, otherwise false
|
||||||
|
*/
|
||||||
|
public boolean isAbove(JavaMinecraftVersion other) {
|
||||||
|
return this.protocol > other.getProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this version is
|
||||||
|
* or above the one given.
|
||||||
|
*
|
||||||
|
* @param other the other version
|
||||||
|
* @return true if is or above, otherwise false
|
||||||
|
*/
|
||||||
|
public boolean isOrAbove(JavaMinecraftVersion other) {
|
||||||
|
return this.protocol >= other.getProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this version is
|
||||||
|
* below the one given.
|
||||||
|
*
|
||||||
|
* @param other the other version
|
||||||
|
* @return true if below, otherwise false
|
||||||
|
*/
|
||||||
|
public boolean isBelow(JavaMinecraftVersion other) {
|
||||||
|
return this.protocol < other.getProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this version is
|
||||||
|
* or below the one given.
|
||||||
|
*
|
||||||
|
* @param other the other version
|
||||||
|
* @return true if is or below, otherwise false
|
||||||
|
*/
|
||||||
|
public boolean isOrBelow(JavaMinecraftVersion other) {
|
||||||
|
return this.protocol <= other.getProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minimum Minecraft version.
|
||||||
|
*
|
||||||
|
* @return the minimum version
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static JavaMinecraftVersion getMinimumVersion() {
|
||||||
|
return VALUES[VALUES.length - 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version from the given protocol.
|
||||||
|
*
|
||||||
|
* @param protocol the protocol to get the version for
|
||||||
|
* @return the version, null if none
|
||||||
|
*/
|
||||||
|
public static JavaMinecraftVersion byProtocol(int protocol) {
|
||||||
|
for (JavaMinecraftVersion version : values()) {
|
||||||
|
if (version.getProtocol() == protocol) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
85
src/main/java/xyz/mcutils/backend/common/MojangServer.java
Normal file
85
src/main/java/xyz/mcutils/backend/common/MojangServer.java
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fascinated (fascinated7)
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public enum MojangServer {
|
||||||
|
SESSION("Session Server", "https://sessionserver.mojang.com"),
|
||||||
|
API("Mojang API", "https://api.mojang.com"),
|
||||||
|
TEXTURES("Textures Server", "https://textures.minecraft.net"),
|
||||||
|
ASSETS("Assets Server", "https://assets.mojang.com"),
|
||||||
|
LIBRARIES("Libraries Server", "https://libraries.minecraft.net"),
|
||||||
|
SERVICES("Minecraft Services", "https://api.minecraftservices.com");
|
||||||
|
|
||||||
|
private static final long STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this server.
|
||||||
|
*/
|
||||||
|
@NonNull private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The endpoint of this service.
|
||||||
|
*/
|
||||||
|
@NonNull private final String endpoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping this service and get the status of it.
|
||||||
|
*
|
||||||
|
* @return the service status
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Status getStatus() {
|
||||||
|
try {
|
||||||
|
InetAddress address = InetAddress.getByName(endpoint.substring(8));
|
||||||
|
long before = System.currentTimeMillis();
|
||||||
|
if (address.isReachable((int) STATUS_TIMEOUT)) {
|
||||||
|
// The time it took to reach the host is 75% of
|
||||||
|
// the timeout, consider it to be degraded.
|
||||||
|
if ((System.currentTimeMillis() - before) > STATUS_TIMEOUT * 0.75D) {
|
||||||
|
return Status.DEGRADED;
|
||||||
|
}
|
||||||
|
return Status.ONLINE;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// We can safely ignore any errors, we're simply checking
|
||||||
|
// if the host is reachable, if it's not, then it's offline.
|
||||||
|
}
|
||||||
|
return Status.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of a service.
|
||||||
|
*/
|
||||||
|
public enum Status {
|
||||||
|
/**
|
||||||
|
* The service is online and accessible.
|
||||||
|
*/
|
||||||
|
ONLINE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is online, but is experiencing degraded performance.
|
||||||
|
*/
|
||||||
|
DEGRADED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is offline and inaccessible.
|
||||||
|
*/
|
||||||
|
OFFLINE
|
||||||
|
}
|
||||||
|
}
|
50
src/main/java/xyz/mcutils/backend/common/PlayerUtils.java
Normal file
50
src/main/java/xyz/mcutils/backend/common/PlayerUtils.java
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.Main;
|
||||||
|
import xyz.mcutils.backend.exception.impl.BadRequestException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@UtilityClass @Log4j2(topic = "Player Utils")
|
||||||
|
public class PlayerUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the UUID from the string.
|
||||||
|
*
|
||||||
|
* @param id the id string
|
||||||
|
* @return the UUID
|
||||||
|
*/
|
||||||
|
public static UUID getUuidFromString(String id) {
|
||||||
|
UUID uuid;
|
||||||
|
boolean isFullUuid = id.length() == 36;
|
||||||
|
if (id.length() == 32 || isFullUuid) {
|
||||||
|
try {
|
||||||
|
uuid = isFullUuid ? UUID.fromString(id) : UUIDUtils.addDashes(id);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new BadRequestException("Invalid UUID provided: %s".formatted(id));
|
||||||
|
}
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the skin data from the URL.
|
||||||
|
*
|
||||||
|
* @return the skin data
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
@JsonIgnore
|
||||||
|
public static byte[] getSkinImage(String url) {
|
||||||
|
HttpResponse<byte[]> response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(),
|
||||||
|
HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
return response.body();
|
||||||
|
}
|
||||||
|
}
|
16
src/main/java/xyz/mcutils/backend/common/ServerUtils.java
Normal file
16
src/main/java/xyz/mcutils/backend/common/ServerUtils.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class ServerUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the address of the server.
|
||||||
|
*
|
||||||
|
* @return the address of the server
|
||||||
|
*/
|
||||||
|
public static String getAddress(String ip, int port) {
|
||||||
|
return ip + (port == 25565 ? "" : ":" + port);
|
||||||
|
}
|
||||||
|
}
|
19
src/main/java/xyz/mcutils/backend/common/Timer.java
Normal file
19
src/main/java/xyz/mcutils/backend/common/Timer.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
public class Timer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a task to run after a delay.
|
||||||
|
*
|
||||||
|
* @param runnable the task to run
|
||||||
|
* @param delay the delay before the task runs
|
||||||
|
*/
|
||||||
|
public static void scheduleRepeating(Runnable runnable, long delay, long period) {
|
||||||
|
new java.util.Timer().scheduleAtFixedRate(new java.util.TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
}, delay, period);
|
||||||
|
}
|
||||||
|
}
|
2
src/main/java/cc/fascinated/common/Tuple.java → src/main/java/xyz/mcutils/backend/common/Tuple.java
2
src/main/java/cc/fascinated/common/Tuple.java → src/main/java/xyz/mcutils/backend/common/Tuple.java
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.common;
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
36
src/main/java/xyz/mcutils/backend/common/UUIDUtils.java
Normal file
36
src/main/java/xyz/mcutils/backend/common/UUIDUtils.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class UUIDUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add dashes to a UUID.
|
||||||
|
*
|
||||||
|
* @param trimmed the UUID without dashes
|
||||||
|
* @return the UUID with dashes
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static UUID addDashes(@NonNull String trimmed) {
|
||||||
|
StringBuilder builder = new StringBuilder(trimmed);
|
||||||
|
for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
|
||||||
|
builder.insert(pos, "-");
|
||||||
|
}
|
||||||
|
return UUID.fromString(builder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove dashes from a UUID.
|
||||||
|
*
|
||||||
|
* @param dashed the UUID with dashes
|
||||||
|
* @return the UUID without dashes
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static String removeDashes(@NonNull UUID dashed) {
|
||||||
|
return dashed.toString().replace("-", "");
|
||||||
|
}
|
||||||
|
}
|
77
src/main/java/xyz/mcutils/backend/common/WebRequest.java
Normal file
77
src/main/java/xyz/mcutils/backend/common/WebRequest.java
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package xyz.mcutils.backend.common;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import xyz.mcutils.backend.exception.impl.RateLimitException;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class WebRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The web client.
|
||||||
|
*/
|
||||||
|
private static final RestClient CLIENT;
|
||||||
|
|
||||||
|
static {
|
||||||
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
requestFactory.setConnectTimeout(2500); // 2.5 seconds
|
||||||
|
CLIENT = RestClient.builder()
|
||||||
|
.requestFactory(requestFactory)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a response from the given URL.
|
||||||
|
*
|
||||||
|
* @param url the url
|
||||||
|
* @return the response
|
||||||
|
* @param <T> the type of the response
|
||||||
|
*/
|
||||||
|
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
|
||||||
|
ResponseEntity<T> responseEntity = CLIENT.get()
|
||||||
|
.uri(url)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||||
|
.toEntity(clazz);
|
||||||
|
|
||||||
|
if (responseEntity.getStatusCode().isError()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (responseEntity.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
|
||||||
|
throw new RateLimitException("Rate limit reached");
|
||||||
|
}
|
||||||
|
return responseEntity.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a response from the given URL.
|
||||||
|
*
|
||||||
|
* @param url the url
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
public static ResponseEntity<?> get(String url, Class<?> clazz) {
|
||||||
|
return CLIENT.get()
|
||||||
|
.uri(url)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||||
|
.toEntity(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a response from the given URL.
|
||||||
|
*
|
||||||
|
* @param url the url
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
public static ResponseEntity<?> head(String url, Class<?> clazz) {
|
||||||
|
return CLIENT.head()
|
||||||
|
.uri(url)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
|
||||||
|
.toEntity(clazz);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package xyz.mcutils.backend.common.packet;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a packet in the
|
||||||
|
* Minecraft Bedrock protocol.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Raknet_Protocol">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
public interface MinecraftBedrockPacket {
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param socket the socket to process the packet for
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
void process(@NonNull DatagramSocket socket) throws IOException;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.common.packet;
|
package xyz.mcutils.backend.common.packet;
|
||||||
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
42
src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java
Normal file
42
src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPing.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package xyz.mcutils.backend.common.packet.impl.bedrock;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This packet is sent by the client to the server to
|
||||||
|
* request a pong response from the server. The server
|
||||||
|
* will respond with a string containing the server's status.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Ping">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
public final class BedrockPacketUnconnectedPing implements MinecraftBedrockPacket {
|
||||||
|
private static final byte ID = 0x01; // The ID of the packet
|
||||||
|
private static final byte[] MAGIC = { 0, -1, -1, 0, -2, -2, -2, -2, -3, -3, -3, -3, 18, 52, 86, 120 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param socket the socket to process the packet for
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void process(@NonNull DatagramSocket socket) throws IOException {
|
||||||
|
// Construct the packet buffer
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(33).order(ByteOrder.LITTLE_ENDIAN);;
|
||||||
|
buffer.put(ID); // Packet ID
|
||||||
|
buffer.putLong(System.currentTimeMillis()); // Timestamp
|
||||||
|
buffer.put(MAGIC); // Magic
|
||||||
|
buffer.putLong(0L); // Client GUID
|
||||||
|
|
||||||
|
// Send the packet
|
||||||
|
socket.send(new DatagramPacket(buffer.array(), 0, buffer.limit()));
|
||||||
|
}
|
||||||
|
}
|
62
src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java
Normal file
62
src/main/java/xyz/mcutils/backend/common/packet/impl/bedrock/BedrockPacketUnconnectedPong.java
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package xyz.mcutils.backend.common.packet.impl.bedrock;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import xyz.mcutils.backend.common.packet.MinecraftBedrockPacket;
|
||||||
|
import xyz.mcutils.backend.model.server.BedrockMinecraftServer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This packet is sent by the server to the client in
|
||||||
|
* response to the {@link BedrockPacketUnconnectedPing}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
* @see <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Protocol Docs</a>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public final class BedrockPacketUnconnectedPong implements MinecraftBedrockPacket {
|
||||||
|
private static final byte ID = 0x1C; // The ID of the packet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The response from the server, null if none.
|
||||||
|
*/
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process this packet.
|
||||||
|
*
|
||||||
|
* @param socket the socket to process the packet for
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void process(@NonNull DatagramSocket socket) throws IOException {
|
||||||
|
// Handle receiving of the packet
|
||||||
|
byte[] receiveData = new byte[2048];
|
||||||
|
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
|
||||||
|
socket.receive(receivePacket);
|
||||||
|
|
||||||
|
// Construct a buffer from the received packet
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(receivePacket.getData()).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
byte id = buffer.get(); // The received packet id
|
||||||
|
if (id == ID) {
|
||||||
|
String response = new String(buffer.array(), StandardCharsets.UTF_8).trim(); // Extract the response
|
||||||
|
|
||||||
|
// Trim the length of the response (short) from the
|
||||||
|
// start of the string, which begins with the edition name
|
||||||
|
for (BedrockMinecraftServer.Edition edition : BedrockMinecraftServer.Edition.values()) {
|
||||||
|
int startIndex = response.indexOf(edition.name());
|
||||||
|
if (startIndex != -1) {
|
||||||
|
response = response.substring(startIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package cc.fascinated.common.packet.impl.java;
|
package xyz.mcutils.backend.common.packet.impl.java;
|
||||||
|
|
||||||
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
@ -1,8 +1,8 @@
|
|||||||
package cc.fascinated.common.packet.impl.java;
|
package xyz.mcutils.backend.common.packet.impl.java;
|
||||||
|
|
||||||
import cc.fascinated.common.packet.MinecraftJavaPacket;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import xyz.mcutils.backend.common.packet.MinecraftJavaPacket;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
@ -0,0 +1,27 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer;
|
||||||
|
|
||||||
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public abstract class IsometricSkinRenderer<T extends ISkinPart> extends SkinRenderer<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a part onto the texture.
|
||||||
|
*
|
||||||
|
* @param graphics the graphics to draw to
|
||||||
|
* @param partImage the part image to draw
|
||||||
|
* @param transform the transform to apply
|
||||||
|
* @param x the x position to draw at
|
||||||
|
* @param y the y position to draw at
|
||||||
|
* @param width the part image width
|
||||||
|
* @param height the part image height
|
||||||
|
*/
|
||||||
|
protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform,
|
||||||
|
double x, double y, int width, int height) {
|
||||||
|
graphics.setTransform(transform);
|
||||||
|
graphics.drawImage(partImage, (int) x, (int) y, width, height, null);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public abstract class Renderer<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the object to the specified size.
|
||||||
|
*
|
||||||
|
* @param input The object to render.
|
||||||
|
* @param size The size to render the object to.
|
||||||
|
*/
|
||||||
|
public abstract BufferedImage render(T input, int size);
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.common.ImageUtils;
|
||||||
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
@Log4j2(topic = "Skin Renderer")
|
||||||
|
public abstract class SkinRenderer<T extends ISkinPart> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the texture of a part of the skin.
|
||||||
|
*
|
||||||
|
* @param skin the skin to get the part texture from
|
||||||
|
* @param part the part of the skin to get
|
||||||
|
* @param size the size to scale the texture to
|
||||||
|
* @param renderOverlays should the overlays be rendered
|
||||||
|
* @return the texture of the skin part
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public BufferedImage getVanillaSkinPart(Skin skin, ISkinPart.Vanilla part, double size, boolean renderOverlays) {
|
||||||
|
ISkinPart.Vanilla.Coordinates coordinates = part.getCoordinates(); // The coordinates of the part
|
||||||
|
|
||||||
|
// The skin texture is legacy, use legacy coordinates
|
||||||
|
if (skin.isLegacy() && part.hasLegacyCoordinates()) {
|
||||||
|
coordinates = part.getLegacyCoordinates();
|
||||||
|
}
|
||||||
|
int width = part.getWidth(); // The width of the part
|
||||||
|
if (skin.getModel() == Skin.Model.SLIM && part.isFrontArm()) {
|
||||||
|
width--;
|
||||||
|
}
|
||||||
|
BufferedImage skinImage = ImageIO.read(new ByteArrayInputStream(skin.getSkinImage())); // The skin texture
|
||||||
|
BufferedImage partTexture = getSkinPartTexture(skinImage, coordinates.getX(), coordinates.getY(), width, part.getHeight(), size);
|
||||||
|
if (coordinates instanceof ISkinPart.Vanilla.LegacyCoordinates legacyCoordinates && legacyCoordinates.isFlipped()) {
|
||||||
|
partTexture = ImageUtils.flip(partTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw part overlays
|
||||||
|
ISkinPart.Vanilla[] overlayParts = part.getOverlays();
|
||||||
|
if (overlayParts != null && renderOverlays) {
|
||||||
|
log.info("Applying overlays to part: {}", part.name());
|
||||||
|
for (ISkinPart.Vanilla overlay : overlayParts) {
|
||||||
|
applyOverlay(partTexture.createGraphics(), getVanillaSkinPart(skin, overlay, size, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return partTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the texture of a specific part of the skin.
|
||||||
|
*
|
||||||
|
* @param skinImage the skin image to get the part from
|
||||||
|
* @param x the x position of the part
|
||||||
|
* @param y the y position of the part
|
||||||
|
* @param width the width of the part
|
||||||
|
* @param height the height of the part
|
||||||
|
* @param size the size to scale the part to
|
||||||
|
* @return the texture of the skin part
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
private BufferedImage getSkinPartTexture(BufferedImage skinImage, int x, int y, int width, int height, double size) {
|
||||||
|
// Create a new BufferedImage for the part of the skin texture
|
||||||
|
BufferedImage headTexture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
|
||||||
|
// Crop just the part we want based on our x, y, width, and height
|
||||||
|
headTexture.getGraphics().drawImage(skinImage, 0, 0, width, height, x, y, x + width, y + height, null);
|
||||||
|
|
||||||
|
// Scale the skin part texture
|
||||||
|
if (size > 0D) {
|
||||||
|
headTexture = ImageUtils.resize(headTexture, size);
|
||||||
|
}
|
||||||
|
return headTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an overlay to a texture.
|
||||||
|
*
|
||||||
|
* @param graphics the graphics to overlay on
|
||||||
|
* @param overlayImage the part to overlay
|
||||||
|
*/
|
||||||
|
protected void applyOverlay(Graphics2D graphics, BufferedImage overlayImage) {
|
||||||
|
try {
|
||||||
|
graphics.drawImage(overlayImage, 0, 0, null);
|
||||||
|
graphics.dispose();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// We can safely ignore this, legacy
|
||||||
|
// skins don't have overlays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the skin part for the player's skin.
|
||||||
|
*
|
||||||
|
* @param skin the player's skin
|
||||||
|
* @param part the skin part to render
|
||||||
|
* @param renderOverlays should the overlays be rendered
|
||||||
|
* @param size the size of the part
|
||||||
|
* @return the rendered skin part
|
||||||
|
*/
|
||||||
|
public abstract BufferedImage render(Skin skin, T part, boolean renderOverlays, int size);
|
||||||
|
}
|
176
src/main/java/xyz/mcutils/backend/common/renderer/impl/server/ServerPreviewRenderer.java
Normal file
176
src/main/java/xyz/mcutils/backend/common/renderer/impl/server/ServerPreviewRenderer.java
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer.impl.server;
|
||||||
|
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.Main;
|
||||||
|
import xyz.mcutils.backend.common.ColorUtils;
|
||||||
|
import xyz.mcutils.backend.common.Fonts;
|
||||||
|
import xyz.mcutils.backend.common.ImageUtils;
|
||||||
|
import xyz.mcutils.backend.common.renderer.Renderer;
|
||||||
|
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||||
|
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||||
|
import xyz.mcutils.backend.service.ServerService;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
@Log4j2
|
||||||
|
public class ServerPreviewRenderer extends Renderer<MinecraftServer> {
|
||||||
|
public static final ServerPreviewRenderer INSTANCE = new ServerPreviewRenderer();
|
||||||
|
|
||||||
|
|
||||||
|
private static BufferedImage SERVER_BACKGROUND;
|
||||||
|
private static BufferedImage PING_ICON;
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
SERVER_BACKGROUND = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/server_background.png").readAllBytes()));
|
||||||
|
PING_ICON = ImageIO.read(new ByteArrayInputStream(Main.class.getResourceAsStream("/icons/ping.png").readAllBytes()));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to load server preview assets", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int fontSize = Fonts.MINECRAFT.getSize();
|
||||||
|
private final int width = 560;
|
||||||
|
private final int height = 64 + 3 + 3;
|
||||||
|
private final int padding = 3;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(MinecraftServer server, int size) {
|
||||||
|
BufferedImage texture = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||||
|
BufferedImage favicon = getServerFavicon(server);
|
||||||
|
BufferedImage background = SERVER_BACKGROUND;
|
||||||
|
|
||||||
|
// Create the graphics for drawing
|
||||||
|
Graphics2D graphics = texture.createGraphics();
|
||||||
|
|
||||||
|
// Set up the font
|
||||||
|
graphics.setFont(Fonts.MINECRAFT);
|
||||||
|
|
||||||
|
// Draw the background
|
||||||
|
for (int backgroundX = 0; backgroundX < width + background.getWidth(); backgroundX += background.getWidth()) {
|
||||||
|
for (int backgroundY = 0; backgroundY < height + background.getHeight(); backgroundY += background.getHeight()) {
|
||||||
|
graphics.drawImage(background, backgroundX, backgroundY, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int y = fontSize + 1;
|
||||||
|
int x = 64 + 8;
|
||||||
|
int initialX = x; // Store the initial value of x
|
||||||
|
|
||||||
|
// Draw the favicon
|
||||||
|
graphics.drawImage(favicon, padding, padding, null);
|
||||||
|
|
||||||
|
// Draw the server hostname
|
||||||
|
graphics.setColor(Color.WHITE);
|
||||||
|
graphics.drawString(server.getHostname(), x, y);
|
||||||
|
|
||||||
|
// Draw the server motd
|
||||||
|
y += fontSize + (padding * 2);
|
||||||
|
for (String line : server.getMotd().getRaw()) {
|
||||||
|
int index = 0;
|
||||||
|
int colorIndex = line.indexOf("§");
|
||||||
|
while (colorIndex != -1) {
|
||||||
|
// Draw text before color code
|
||||||
|
String textBeforeColor = line.substring(index, colorIndex);
|
||||||
|
graphics.drawString(textBeforeColor, x, y);
|
||||||
|
// Calculate width of text before color code
|
||||||
|
int textWidth = graphics.getFontMetrics().stringWidth(textBeforeColor);
|
||||||
|
// Move x position to after the drawn text
|
||||||
|
x += textWidth;
|
||||||
|
// Set color based on color code
|
||||||
|
char colorCode = Character.toLowerCase(line.charAt(colorIndex + 1));
|
||||||
|
|
||||||
|
// Set the color and font style
|
||||||
|
switch (colorCode) {
|
||||||
|
case 'l': graphics.setFont(Fonts.MINECRAFT_BOLD);
|
||||||
|
case 'o': graphics.setFont(Fonts.MINECRAFT_ITALIC);
|
||||||
|
default: {
|
||||||
|
try {
|
||||||
|
graphics.setFont(Fonts.MINECRAFT);
|
||||||
|
Color color = ColorUtils.getMinecraftColor(colorCode);
|
||||||
|
graphics.setColor(color);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Unknown color, can ignore the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move index to after the color code
|
||||||
|
index = colorIndex + 2;
|
||||||
|
// Find next color code
|
||||||
|
colorIndex = line.indexOf("§", index);
|
||||||
|
}
|
||||||
|
// Draw remaining text
|
||||||
|
String remainingText = line.substring(index);
|
||||||
|
graphics.drawString(remainingText, x, y);
|
||||||
|
// Move to the next line
|
||||||
|
y += fontSize + padding;
|
||||||
|
// Reset x position for the next line
|
||||||
|
x = initialX; // Reset x to its initial value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the font is reset
|
||||||
|
graphics.setFont(Fonts.MINECRAFT);
|
||||||
|
|
||||||
|
// Render the ping
|
||||||
|
BufferedImage pingIcon = ImageUtils.resize(PING_ICON, 2);
|
||||||
|
x = width - pingIcon.getWidth() - padding;
|
||||||
|
graphics.drawImage(pingIcon, x, padding, null);
|
||||||
|
|
||||||
|
// Reset the y position
|
||||||
|
y = fontSize + 1;
|
||||||
|
|
||||||
|
// Render the player count
|
||||||
|
MinecraftServer.Players players = server.getPlayers();
|
||||||
|
String playersOnline = players.getOnline() + "";
|
||||||
|
String playersMax = players.getMax() + "";
|
||||||
|
|
||||||
|
// Calculate the width of each player count element
|
||||||
|
int maxWidth = graphics.getFontMetrics().stringWidth(playersMax);
|
||||||
|
int slashWidth = graphics.getFontMetrics().stringWidth("/");
|
||||||
|
int onlineWidth = graphics.getFontMetrics().stringWidth(playersOnline);
|
||||||
|
|
||||||
|
// Calculate the total width of the player count string
|
||||||
|
int totalWidth = maxWidth + slashWidth + onlineWidth;
|
||||||
|
|
||||||
|
// Calculate the starting x position
|
||||||
|
int startX = (width - totalWidth) - pingIcon.getWidth() - 6;
|
||||||
|
|
||||||
|
// Render the player count elements
|
||||||
|
graphics.setColor(Color.LIGHT_GRAY);
|
||||||
|
graphics.drawString(playersOnline, startX, y);
|
||||||
|
startX += onlineWidth;
|
||||||
|
graphics.setColor(Color.DARK_GRAY);
|
||||||
|
graphics.drawString("/", startX, y);
|
||||||
|
startX += slashWidth;
|
||||||
|
graphics.setColor(Color.LIGHT_GRAY);
|
||||||
|
graphics.drawString(playersMax, startX, y);
|
||||||
|
|
||||||
|
return ImageUtils.resize(texture, (double) size / width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the favicon of a server.
|
||||||
|
*
|
||||||
|
* @param server the server to get the favicon of
|
||||||
|
* @return the server favicon
|
||||||
|
*/
|
||||||
|
public BufferedImage getServerFavicon(MinecraftServer server) {
|
||||||
|
String favicon = null;
|
||||||
|
|
||||||
|
// Get the server favicon
|
||||||
|
if (server instanceof JavaMinecraftServer javaServer) {
|
||||||
|
if (javaServer.getFavicon() != null) {
|
||||||
|
favicon = javaServer.getFavicon().getBase64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to the default server icon
|
||||||
|
if (favicon == null) {
|
||||||
|
favicon = ServerService.DEFAULT_SERVER_ICON;
|
||||||
|
}
|
||||||
|
return ImageUtils.base64ToImage(favicon);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.common.ImageUtils;
|
||||||
|
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
||||||
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Body")
|
||||||
|
public class BodyRenderer extends SkinRenderer<ISkinPart.Custom> {
|
||||||
|
public static final BodyRenderer INSTANCE = new BodyRenderer();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
|
||||||
|
BufferedImage texture = new BufferedImage(16, 32, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||||
|
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||||
|
|
||||||
|
// Get the Vanilla skin parts to draw
|
||||||
|
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, -1, renderOverlays);
|
||||||
|
BufferedImage body = getVanillaSkinPart(skin, ISkinPart.Vanilla.BODY_FRONT, -1, renderOverlays);
|
||||||
|
BufferedImage leftArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_ARM_FRONT, -1, renderOverlays);
|
||||||
|
BufferedImage rightArm = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_ARM_FRONT, -1, renderOverlays);
|
||||||
|
BufferedImage leftLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.LEFT_LEG_FRONT, -1, renderOverlays);
|
||||||
|
BufferedImage rightLeg = getVanillaSkinPart(skin, ISkinPart.Vanilla.RIGHT_LEG_FRONT, -1, renderOverlays);
|
||||||
|
|
||||||
|
// Draw the body parts
|
||||||
|
graphics.drawImage(face, 4, 0, null);
|
||||||
|
graphics.drawImage(body, 4, 8, null);
|
||||||
|
graphics.drawImage(leftArm, skin.getModel() == Skin.Model.SLIM ? 1 : 0, 8, null);
|
||||||
|
graphics.drawImage(rightArm, 12, 8, null);
|
||||||
|
graphics.drawImage(leftLeg, 8, 20, null);
|
||||||
|
graphics.drawImage(rightLeg, 4, 20, null);
|
||||||
|
|
||||||
|
graphics.dispose();
|
||||||
|
return ImageUtils.resize(texture, (double) size / 32);
|
||||||
|
}
|
||||||
|
}
|
48
src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java
Normal file
48
src/main/java/xyz/mcutils/backend/common/renderer/impl/skin/IsometricHeadRenderer.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||||
|
|
||||||
|
import xyz.mcutils.backend.common.renderer.IsometricSkinRenderer;
|
||||||
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public class IsometricHeadRenderer extends IsometricSkinRenderer<ISkinPart.Custom> {
|
||||||
|
public static final IsometricHeadRenderer INSTANCE = new IsometricHeadRenderer();
|
||||||
|
|
||||||
|
private static final double SKEW_A = 26D / 45D; // 0.57777777
|
||||||
|
private static final double SKEW_B = SKEW_A * 2D; // 1.15555555
|
||||||
|
|
||||||
|
private static final AffineTransform HEAD_TOP_TRANSFORM = new AffineTransform(1D, -SKEW_A, 1, SKEW_A, 0, 0);
|
||||||
|
private static final AffineTransform FACE_TRANSFORM = new AffineTransform(1D, -SKEW_A, 0D, SKEW_B, 0d, SKEW_A);
|
||||||
|
private static final AffineTransform HEAD_LEFT_TRANSFORM = new AffineTransform(1D, SKEW_A, 0D, SKEW_B, 0D, 0D);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(Skin skin, ISkinPart.Custom part, boolean renderOverlays, int size) {
|
||||||
|
double scale = (size / 8D) / 2.5;
|
||||||
|
double zOffset = scale * 3.5D;
|
||||||
|
double xOffset = scale * 2D;
|
||||||
|
|
||||||
|
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||||
|
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||||
|
|
||||||
|
// Get the Vanilla skin parts to draw
|
||||||
|
BufferedImage headTop = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_TOP, scale, renderOverlays);
|
||||||
|
BufferedImage face = getVanillaSkinPart(skin, ISkinPart.Vanilla.FACE, scale, renderOverlays);
|
||||||
|
BufferedImage headLeft = getVanillaSkinPart(skin, ISkinPart.Vanilla.HEAD_LEFT, scale, renderOverlays);
|
||||||
|
|
||||||
|
// Draw the top head part
|
||||||
|
drawPart(graphics, headTop, HEAD_TOP_TRANSFORM, -0.5 - zOffset, xOffset + zOffset, headTop.getWidth(), headTop.getHeight() + 2);
|
||||||
|
|
||||||
|
// Draw the face part
|
||||||
|
double x = xOffset + 8 * scale;
|
||||||
|
drawPart(graphics, face, FACE_TRANSFORM, x, x + zOffset - 0.5, face.getWidth(), face.getHeight());
|
||||||
|
|
||||||
|
// Draw the left head part
|
||||||
|
drawPart(graphics, headLeft, HEAD_LEFT_TRANSFORM, xOffset + 1, zOffset - 0.5, headLeft.getWidth(), headLeft.getHeight());
|
||||||
|
|
||||||
|
graphics.dispose();
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package xyz.mcutils.backend.common.renderer.impl.skin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
||||||
|
import xyz.mcutils.backend.model.skin.ISkinPart;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter @Log4j2(topic = "Skin Renderer/Square")
|
||||||
|
public class SquareRenderer extends SkinRenderer<ISkinPart.Vanilla> {
|
||||||
|
public static final SquareRenderer INSTANCE = new SquareRenderer();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(Skin skin, ISkinPart.Vanilla part, boolean renderOverlays, int size) {
|
||||||
|
double scale = size / 8D;
|
||||||
|
BufferedImage partImage = getVanillaSkinPart(skin, part, scale, renderOverlays); // Get the part image
|
||||||
|
if (!renderOverlays) { // Not rendering overlays
|
||||||
|
return partImage;
|
||||||
|
}
|
||||||
|
// Create a new image, draw our skin part texture, and then apply overlays
|
||||||
|
BufferedImage texture = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); // The texture to return
|
||||||
|
Graphics2D graphics = texture.createGraphics(); // Create the graphics for drawing
|
||||||
|
graphics.drawImage(partImage, 0, 0, null);
|
||||||
|
|
||||||
|
graphics.dispose();
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
}
|
54
src/main/java/xyz/mcutils/backend/config/Config.java
Normal file
54
src/main/java/xyz/mcutils/backend/config/Config.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package xyz.mcutils.backend.config;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Getter @Log4j2(topic = "Config")
|
||||||
|
@Configuration
|
||||||
|
public class Config {
|
||||||
|
public static Config INSTANCE;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Environment environment;
|
||||||
|
|
||||||
|
@Value("${public-url}")
|
||||||
|
private String webPublicUrl;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void onInitialize() {
|
||||||
|
INSTANCE = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
|
||||||
|
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
|
||||||
|
filterRegistrationBean.addUrlPatterns("/*");
|
||||||
|
filterRegistrationBean.setName("etagFilter");
|
||||||
|
return filterRegistrationBean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcConfigurer configureCors() {
|
||||||
|
return new WebMvcConfigurer() {
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||||
|
// Allow all origins to access the API
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOrigins("*") // Allow all origins
|
||||||
|
.allowedMethods("*") // Allow all methods
|
||||||
|
.allowedHeaders("*"); // Allow all headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
15
src/main/java/xyz/mcutils/backend/config/MongoConfig.java
Normal file
15
src/main/java/xyz/mcutils/backend/config/MongoConfig.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package xyz.mcutils.backend.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||||
|
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableMongoRepositories(basePackages = "xyz.mcutils.backend.repository.mongo")
|
||||||
|
public class MongoConfig {
|
||||||
|
@Autowired
|
||||||
|
void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
|
||||||
|
mappingMongoConverter.setMapKeyDotReplacement("-DOT");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package xyz.mcutils.backend.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.info.BuildProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenAPIConfiguration {
|
||||||
|
/**
|
||||||
|
* The build properties of the
|
||||||
|
* app, null if the app is not built.
|
||||||
|
*/
|
||||||
|
private final BuildProperties buildProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public OpenAPIConfiguration(BuildProperties buildProperties) {
|
||||||
|
this.buildProperties = buildProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI defineOpenAPI() {
|
||||||
|
Server server = new Server();
|
||||||
|
server.setUrl(Config.INSTANCE.getWebPublicUrl());
|
||||||
|
|
||||||
|
Contact contact = new Contact();
|
||||||
|
contact.setName("Liam");
|
||||||
|
contact.setEmail("liam@fascinated.cc");
|
||||||
|
contact.setUrl("https://fascinated.cc");
|
||||||
|
|
||||||
|
Info info = new Info();
|
||||||
|
info.setTitle("Minecraft Utilities API");
|
||||||
|
info.setVersion(buildProperties == null ? "N/A" : buildProperties.getVersion());
|
||||||
|
info.setDescription("Wrapper for the Minecraft APIs to make them easier to use.");
|
||||||
|
info.setContact(contact);
|
||||||
|
info.setLicense(new License().name("MIT License").url("https://opensource.org/licenses/MIT"));
|
||||||
|
|
||||||
|
return new OpenAPI().servers(List.of(server)).info(info);
|
||||||
|
}
|
||||||
|
}
|
75
src/main/java/xyz/mcutils/backend/config/RedisConfig.java
Normal file
75
src/main/java/xyz/mcutils/backend/config/RedisConfig.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package xyz.mcutils.backend.config;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||||
|
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Log4j2(topic = "Redis")
|
||||||
|
@EnableRedisRepositories(basePackages = "xyz.mcutils.backend.repository.redis")
|
||||||
|
public class RedisConfig {
|
||||||
|
/**
|
||||||
|
* The Redis server host.
|
||||||
|
*/
|
||||||
|
@Value("${spring.data.redis.host}")
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redis server port.
|
||||||
|
*/
|
||||||
|
@Value("${spring.data.redis.port}")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redis database index.
|
||||||
|
*/
|
||||||
|
@Value("${spring.data.redis.database}")
|
||||||
|
private int database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional Redis password.
|
||||||
|
*/
|
||||||
|
@Value("${spring.data.redis.auth}")
|
||||||
|
private String auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the config to use for Redis.
|
||||||
|
*
|
||||||
|
* @return the config
|
||||||
|
* @see RedisTemplate for config
|
||||||
|
*/
|
||||||
|
@Bean @NonNull
|
||||||
|
public RedisTemplate<String, Object> redisTemplate() {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(jedisConnectionFactory());
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the connection factory to use
|
||||||
|
* when making connections to Redis.
|
||||||
|
*
|
||||||
|
* @return the built factory
|
||||||
|
* @see JedisConnectionFactory for factory
|
||||||
|
*/
|
||||||
|
@Bean @NonNull
|
||||||
|
public JedisConnectionFactory jedisConnectionFactory() {
|
||||||
|
log.info("Connecting to Redis at {}:{}/{}", host, port, database);
|
||||||
|
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
|
||||||
|
config.setDatabase(database);
|
||||||
|
if (!auth.trim().isEmpty()) { // Auth with our provided password
|
||||||
|
log.info("Using auth...");
|
||||||
|
config.setPassword(auth);
|
||||||
|
}
|
||||||
|
return new JedisConnectionFactory(config);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package xyz.mcutils.backend.controller;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "/")
|
||||||
|
public class HealthController {
|
||||||
|
|
||||||
|
@GetMapping(value = "/health")
|
||||||
|
public ResponseEntity<?> home() {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"status", "OK"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package xyz.mcutils.backend.controller;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import xyz.mcutils.backend.config.Config;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "/")
|
||||||
|
public class HomeController {
|
||||||
|
private final String examplePlayer = "Notch";
|
||||||
|
private final String exampleJavaServer = "aetheria.cc";
|
||||||
|
private final String exampleBedrockServer = "geo.hivebedrock.network";
|
||||||
|
|
||||||
|
@GetMapping(value = "/")
|
||||||
|
public String home(Model model) {
|
||||||
|
String publicUrl = Config.INSTANCE.getWebPublicUrl();
|
||||||
|
|
||||||
|
model.addAttribute("public_url", publicUrl);
|
||||||
|
model.addAttribute("player_example_url", publicUrl + "/player/" + examplePlayer);
|
||||||
|
model.addAttribute("java_server_example_url", publicUrl + "/server/java/" + exampleJavaServer);
|
||||||
|
model.addAttribute("bedrock_server_example_url", publicUrl + "/server/bedrock/" + exampleBedrockServer);
|
||||||
|
model.addAttribute("mojang_endpoint_status_url", publicUrl + "/mojang/status");
|
||||||
|
model.addAttribute("swagger_url", publicUrl + "/swagger-ui.html");
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package xyz.mcutils.backend.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
|
||||||
|
import xyz.mcutils.backend.service.MojangService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
|
||||||
|
public class MojangController {
|
||||||
|
@Autowired
|
||||||
|
private MojangService mojangService;
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/status")
|
||||||
|
public ResponseEntity<?> getStatus() {
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
|
||||||
|
.body(Map.of("endpoints", mojangService.getMojangServerStatus()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package xyz.mcutils.backend.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedPlayer;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedPlayerName;
|
||||||
|
import xyz.mcutils.backend.model.player.Player;
|
||||||
|
import xyz.mcutils.backend.service.PlayerService;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/player/")
|
||||||
|
@Tag(name = "Player Controller", description = "The Player Controller is used to get information about a player.")
|
||||||
|
public class PlayerController {
|
||||||
|
|
||||||
|
private final PlayerService playerService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public PlayerController(PlayerService playerManagerService) {
|
||||||
|
this.playerService = playerManagerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<?> getPlayer(
|
||||||
|
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
|
||||||
|
CachedPlayer player = playerService.getPlayer(id);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||||
|
.body(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/uuid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<CachedPlayerName> getPlayerUuid(
|
||||||
|
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id) {
|
||||||
|
CachedPlayerName player = playerService.usernameToUuid(id);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePublic())
|
||||||
|
.body(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/{part}/{id}")
|
||||||
|
public ResponseEntity<?> getPlayerHead(
|
||||||
|
@Parameter(description = "The part of the skin", example = "head") @PathVariable String part,
|
||||||
|
@Parameter(description = "The UUID or Username of the player", example = "ImFascinated") @PathVariable String id,
|
||||||
|
@Parameter(description = "The size of the image", example = "256") @RequestParam(required = false, defaultValue = "256") int size,
|
||||||
|
@Parameter(description = "Whether to render the skin overlay (skin layers)", example = "false") @RequestParam(required = false, defaultValue = "false") boolean overlays,
|
||||||
|
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
|
||||||
|
CachedPlayer cachedPlayer = playerService.getPlayer(id);
|
||||||
|
Player player = cachedPlayer.getPlayer();
|
||||||
|
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
|
||||||
|
|
||||||
|
// Return the part image
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||||
|
.contentType(MediaType.IMAGE_PNG)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(player.getUsername()))
|
||||||
|
.body(playerService.getSkinPart(player, part, overlays, size).getBytes());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package xyz.mcutils.backend.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.CacheControl;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
||||||
|
import xyz.mcutils.backend.service.MojangService;
|
||||||
|
import xyz.mcutils.backend.service.ServerService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(value = "/server/")
|
||||||
|
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
|
||||||
|
public class ServerController {
|
||||||
|
|
||||||
|
private final ServerService serverService;
|
||||||
|
private final MojangService mojangService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ServerController(ServerService serverService, MojangService mojangService) {
|
||||||
|
this.serverService = serverService;
|
||||||
|
this.mojangService = mojangService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/{platform}/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<CachedMinecraftServer> getServer(
|
||||||
|
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
|
||||||
|
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname) {
|
||||||
|
CachedMinecraftServer server = serverService.getServer(platform, hostname);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
|
||||||
|
.body(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/icon/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||||
|
public ResponseEntity<byte[]> getServerIcon(
|
||||||
|
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
|
||||||
|
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download) {
|
||||||
|
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
|
||||||
|
byte[] favicon = serverService.getServerFavicon(hostname);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||||
|
.contentType(MediaType.IMAGE_PNG)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
|
||||||
|
.body(favicon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/{platform}/preview/{hostname}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||||
|
public ResponseEntity<byte[]> getServerPreview(
|
||||||
|
@Parameter(description = "The platform of the server", example = "java") @PathVariable String platform,
|
||||||
|
@Parameter(description = "The hostname and port of the server", example = "aetheria.cc") @PathVariable String hostname,
|
||||||
|
@Parameter(description = "Whether to download the image") @RequestParam(required = false, defaultValue = "false") boolean download,
|
||||||
|
@Parameter(description = "The size of the image", example = "1024") @RequestParam(required = false, defaultValue = "1024") int size) {
|
||||||
|
String dispositionHeader = download ? "attachment; filename=%s.png" : "inline; filename=%s.png";
|
||||||
|
CachedMinecraftServer server = serverService.getServer(platform, hostname);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
|
||||||
|
.contentType(MediaType.IMAGE_PNG)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(hostname))
|
||||||
|
.body(serverService.getServerPreview(server, platform, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<?> getServerBlockedStatus(
|
||||||
|
@Parameter(description = "The hostname of the server", example = "aetheria.cc") @PathVariable String hostname) {
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic())
|
||||||
|
.body(Map.of(
|
||||||
|
"blocked", mojangService.isServerBlocked(hostname)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
package cc.fascinated.exception;
|
package xyz.mcutils.backend.exception;
|
||||||
|
|
||||||
import cc.fascinated.model.response.Response;
|
|
||||||
import io.micrometer.common.lang.NonNull;
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import io.sentry.Sentry;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
|
import xyz.mcutils.backend.model.response.ErrorResponse;
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
public final class ExceptionControllerAdvice {
|
public final class ExceptionControllerAdvice {
|
||||||
@ -19,7 +21,12 @@ public final class ExceptionControllerAdvice {
|
|||||||
*/
|
*/
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<?> handleException(@NonNull Exception ex) {
|
public ResponseEntity<?> handleException(@NonNull Exception ex) {
|
||||||
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; // Get the HTTP status
|
HttpStatus status = null; // Get the HTTP status
|
||||||
|
if (ex instanceof NoResourceFoundException) { // Not found
|
||||||
|
status = HttpStatus.NOT_FOUND;
|
||||||
|
} else if (ex instanceof UnsupportedOperationException) { // Not implemented
|
||||||
|
status = HttpStatus.NOT_IMPLEMENTED;
|
||||||
|
}
|
||||||
if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation
|
if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation
|
||||||
status = ex.getClass().getAnnotation(ResponseStatus.class).value();
|
status = ex.getClass().getAnnotation(ResponseStatus.class).value();
|
||||||
}
|
}
|
||||||
@ -27,7 +34,14 @@ public final class ExceptionControllerAdvice {
|
|||||||
if (message == null) { // Fallback
|
if (message == null) { // Fallback
|
||||||
message = "An internal error has occurred.";
|
message = "An internal error has occurred.";
|
||||||
}
|
}
|
||||||
ex.printStackTrace(); // Print the stack trace
|
// Print the stack trace if no response status is present
|
||||||
return new Response(status, message).toResponseEntity(); // Return the error response
|
if (status == null) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
}
|
||||||
|
if (status == null) { // Fallback to 500
|
||||||
|
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
Sentry.captureException(ex); // Capture the exception with Sentry
|
||||||
|
}
|
||||||
|
return new ResponseEntity<>(new ErrorResponse(status, message), status);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package xyz.mcutils.backend.exception.impl;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public class BadRequestException extends RuntimeException {
|
||||||
|
|
||||||
|
public BadRequestException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package xyz.mcutils.backend.exception.impl;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
public class InternalServerErrorException extends RuntimeException {
|
||||||
|
|
||||||
|
public InternalServerErrorException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package xyz.mcutils.backend.exception.impl;
|
||||||
|
|
||||||
|
|
||||||
|
public class MojangAPIRateLimitException extends RateLimitException {
|
||||||
|
|
||||||
|
public MojangAPIRateLimitException() {
|
||||||
|
super("Mojang API rate limit exceeded. Please try again later.");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package xyz.mcutils.backend.exception.impl;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
|
public class RateLimitException extends RuntimeException {
|
||||||
|
|
||||||
|
public RateLimitException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package xyz.mcutils.backend.exception.impl;
|
||||||
|
|
||||||
|
import lombok.experimental.StandardException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@StandardException
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public class ResourceNotFoundException extends RuntimeException { }
|
@ -1,35 +1,39 @@
|
|||||||
package cc.fascinated.log;
|
package xyz.mcutils.backend.log;
|
||||||
|
|
||||||
import cc.fascinated.common.IPUtils;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
import org.springframework.http.server.ServletServerHttpRequest;
|
import org.springframework.http.server.ServletServerHttpRequest;
|
||||||
import org.springframework.http.server.ServletServerHttpResponse;
|
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
import xyz.mcutils.backend.common.IPUtils;
|
||||||
|
import xyz.mcutils.backend.service.MetricService;
|
||||||
|
import xyz.mcutils.backend.service.metric.metrics.RequestsPerRouteMetric;
|
||||||
|
import xyz.mcutils.backend.service.metric.metrics.TotalRequestsMetric;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
@Slf4j(topic = "Req/Res Transaction")
|
@Slf4j(topic = "Req Transaction")
|
||||||
public class TransactionLogger implements ResponseBodyAdvice<Object> {
|
public class TransactionLogger implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MetricService metricService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
|
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
|
||||||
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest,
|
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest rawRequest,
|
||||||
@NonNull ServerHttpResponse rawResponse) {
|
@NonNull ServerHttpResponse rawResponse) {
|
||||||
HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest();
|
HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest();
|
||||||
HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse();
|
|
||||||
|
|
||||||
// Get the request ip ip
|
// Get the request ip ip
|
||||||
String ip = IPUtils.getRealIp(request);
|
String ip = IPUtils.getRealIp(request);
|
||||||
@ -40,34 +44,17 @@ public class TransactionLogger implements ResponseBodyAdvice<Object> {
|
|||||||
params.put(entry.getKey(), Arrays.toString(entry.getValue()));
|
params.put(entry.getKey(), Arrays.toString(entry.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting headers
|
// Logging the request
|
||||||
Map<String, String> headers = new HashMap<>();
|
log.info(String.format("[Req] %s | %s | '%s', params=%s",
|
||||||
Enumeration<String> headerNames = request.getHeaderNames();
|
|
||||||
while (headerNames.hasMoreElements()) {
|
|
||||||
String headerName = headerNames.nextElement();
|
|
||||||
headers.put(headerName, request.getHeader(headerName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the request
|
|
||||||
log.info(String.format("[Req] %s | %s | '%s', params=%s, headers=%s",
|
|
||||||
request.getMethod(),
|
request.getMethod(),
|
||||||
ip,
|
ip,
|
||||||
request.getRequestURI(),
|
request.getRequestURI(),
|
||||||
params,
|
params
|
||||||
headers
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// Getting response headers
|
// Increment the metric
|
||||||
headers = new HashMap<>();
|
((TotalRequestsMetric) metricService.getMetric(TotalRequestsMetric.class)).increment();
|
||||||
for (String headerName : response.getHeaderNames()) {
|
((RequestsPerRouteMetric) metricService.getMetric(RequestsPerRouteMetric.class)).increment(request.getRequestURI());
|
||||||
headers.put(headerName, response.getHeader(headerName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the response
|
|
||||||
log.info(String.format("[Res] %s, headers=%s",
|
|
||||||
response.getStatus(),
|
|
||||||
headers
|
|
||||||
));
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
49
src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java
vendored
Normal file
49
src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
import xyz.mcutils.backend.common.CachedResponse;
|
||||||
|
import xyz.mcutils.backend.common.MojangServer;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||||
|
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
|
||||||
|
public class CachedEndpointStatus extends CachedResponse implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id for this endpoint cache.
|
||||||
|
*/
|
||||||
|
@Id @NonNull @JsonIgnore
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The endpoint cache.
|
||||||
|
*/
|
||||||
|
private final List<Map<String, Object>> endpoints;
|
||||||
|
|
||||||
|
public CachedEndpointStatus(@NonNull String id, Map<MojangServer, MojangServer.Status> mojangServers) {
|
||||||
|
super(Cache.defaultCache());
|
||||||
|
this.id = id;
|
||||||
|
this.endpoints = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
|
||||||
|
MojangServer server = entry.getKey();
|
||||||
|
|
||||||
|
Map<String, Object> serverStatus = new HashMap<>();
|
||||||
|
serverStatus.put("name", server.getName());
|
||||||
|
serverStatus.put("endpoint", server.getEndpoint());
|
||||||
|
serverStatus.put("status", entry.getValue().name());
|
||||||
|
endpoints.add(serverStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
src/main/java/xyz/mcutils/backend/model/cache/CachedMinecraftServer.java
vendored
Normal file
39
src/main/java/xyz/mcutils/backend/model/cache/CachedMinecraftServer.java
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
import xyz.mcutils.backend.common.CachedResponse;
|
||||||
|
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||||
|
@RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
|
||||||
|
public class CachedMinecraftServer extends CachedResponse implements Serializable {
|
||||||
|
/**
|
||||||
|
* The id of this cached server.
|
||||||
|
*/
|
||||||
|
@Id @NonNull @JsonIgnore
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cached server.
|
||||||
|
*/
|
||||||
|
@NonNull @JsonUnwrapped
|
||||||
|
private MinecraftServer server;
|
||||||
|
|
||||||
|
public CachedMinecraftServer(@NonNull String id, @NonNull MinecraftServer server) {
|
||||||
|
super(CachedResponse.Cache.defaultCache());
|
||||||
|
this.id = id;
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayer.java
vendored
Normal file
41
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayer.java
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
import xyz.mcutils.backend.common.CachedResponse;
|
||||||
|
import xyz.mcutils.backend.model.player.Player;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cacheable {@link Player}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||||
|
@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
|
||||||
|
public class CachedPlayer extends CachedResponse implements Serializable {
|
||||||
|
/**
|
||||||
|
* The unique id of the player.
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
@Id private UUID uniqueId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The player to cache.
|
||||||
|
*/
|
||||||
|
@JsonUnwrapped
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
public CachedPlayer(UUID uniqueId, Player player) {
|
||||||
|
super(Cache.defaultCache());
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
}
|
42
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerName.java
vendored
Normal file
42
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerName.java
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
import xyz.mcutils.backend.common.CachedResponse;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
@Getter @EqualsAndHashCode(callSuper = false)
|
||||||
|
@RedisHash(value = "playerName", timeToLive = 60L * 60L * 6) // 6 hours (in seconds)
|
||||||
|
public class CachedPlayerName extends CachedResponse {
|
||||||
|
/**
|
||||||
|
* The id of the player.
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
@Id private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the player.
|
||||||
|
*/
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique id of the player.
|
||||||
|
*/
|
||||||
|
private final UUID uniqueId;
|
||||||
|
|
||||||
|
public CachedPlayerName(String id, String username, UUID uniqueId) {
|
||||||
|
super(Cache.defaultCache());
|
||||||
|
this.id = id;
|
||||||
|
this.username = username;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
}
|
21
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerSkinPart.java
vendored
Normal file
21
src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerSkinPart.java
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Setter @Getter @EqualsAndHashCode
|
||||||
|
@RedisHash(value = "playerSkinPart", timeToLive = 60L * 60L) // 1 hour (in seconds)
|
||||||
|
public class CachedPlayerSkinPart {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the skin part
|
||||||
|
*/
|
||||||
|
@Id @NonNull private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The skin part bytes
|
||||||
|
*/
|
||||||
|
private byte[] bytes;
|
||||||
|
}
|
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
21
src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package xyz.mcutils.backend.model.cache;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Setter @Getter @EqualsAndHashCode
|
||||||
|
@RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds)
|
||||||
|
public class CachedServerPreview {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the server preview
|
||||||
|
*/
|
||||||
|
@Id @NonNull private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server preview bytes
|
||||||
|
*/
|
||||||
|
private byte[] bytes;
|
||||||
|
}
|
26
src/main/java/xyz/mcutils/backend/model/dns/DNSRecord.java
Normal file
26
src/main/java/xyz/mcutils/backend/model/dns/DNSRecord.java
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package xyz.mcutils.backend.model.dns;
|
||||||
|
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
@NoArgsConstructor @AllArgsConstructor
|
||||||
|
@Setter @Getter @EqualsAndHashCode
|
||||||
|
public abstract class DNSRecord {
|
||||||
|
/**
|
||||||
|
* The type of this record.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Type type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The TTL (Time To Live) of this record.
|
||||||
|
*/
|
||||||
|
private long ttl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of a record.
|
||||||
|
*/
|
||||||
|
public enum Type {
|
||||||
|
A, SRV
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package xyz.mcutils.backend.model.dns.impl;
|
||||||
|
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
@Setter @Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public final class ARecord extends DNSRecord {
|
||||||
|
/**
|
||||||
|
* The address of this record, null if unresolved.
|
||||||
|
*/
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
public ARecord(@NonNull org.xbill.DNS.ARecord bootstrap) {
|
||||||
|
super(Type.A, bootstrap.getTTL());
|
||||||
|
InetAddress address = bootstrap.getAddress();
|
||||||
|
this.address = address == null ? null : address.getHostAddress();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package xyz.mcutils.backend.model.dns.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
|
@Setter @Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public final class SRVRecord extends DNSRecord {
|
||||||
|
/**
|
||||||
|
* The priority of this record.
|
||||||
|
*/
|
||||||
|
private int priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The weight of this record.
|
||||||
|
*/
|
||||||
|
private int weight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port of this record.
|
||||||
|
*/
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target of this record.
|
||||||
|
*/
|
||||||
|
@NonNull private String target;
|
||||||
|
|
||||||
|
public SRVRecord(@NonNull org.xbill.DNS.SRVRecord bootstrap) {
|
||||||
|
super(Type.SRV, bootstrap.getTTL());
|
||||||
|
priority = bootstrap.getPriority();
|
||||||
|
weight = bootstrap.getWeight();
|
||||||
|
port = bootstrap.getPort();
|
||||||
|
target = bootstrap.getTarget().toString().replaceFirst("\\.$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a socket address from
|
||||||
|
* the target and port.
|
||||||
|
*
|
||||||
|
* @return the socket address
|
||||||
|
*/
|
||||||
|
@NonNull @JsonIgnore
|
||||||
|
public InetSocketAddress getSocketAddress() {
|
||||||
|
return new InetSocketAddress(target, port);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package xyz.mcutils.backend.model.metric;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class WebsocketMetrics {
|
||||||
|
/**
|
||||||
|
* The metrics to send to the client.
|
||||||
|
*/
|
||||||
|
private final Map<String, Object> metrics;
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package xyz.mcutils.backend.model.mojang;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter @Setter @EqualsAndHashCode
|
||||||
|
public class EndpointStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the service.
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hostname of the service.
|
||||||
|
*/
|
||||||
|
private final String hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the service.
|
||||||
|
*/
|
||||||
|
private Status status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statuses for the endpoint.
|
||||||
|
*/
|
||||||
|
public enum Status {
|
||||||
|
/**
|
||||||
|
* The service is online and operational.
|
||||||
|
*/
|
||||||
|
ONLINE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is online, but may be experiencing issues.
|
||||||
|
* This could be due to high load or other issues.
|
||||||
|
*/
|
||||||
|
DEGRADED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service is offline and not operational.
|
||||||
|
*/
|
||||||
|
OFFLINE
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
package cc.fascinated.model.player;
|
package xyz.mcutils.backend.model.player;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@Getter @AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Getter @EqualsAndHashCode
|
||||||
public class Cape {
|
public class Cape {
|
||||||
|
|
||||||
/**
|
/**
|
63
src/main/java/xyz/mcutils/backend/model/player/Player.java
Normal file
63
src/main/java/xyz/mcutils/backend/model/player/Player.java
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package xyz.mcutils.backend.model.player;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import xyz.mcutils.backend.common.Tuple;
|
||||||
|
import xyz.mcutils.backend.common.UUIDUtils;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
import xyz.mcutils.backend.model.token.MojangProfileToken;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@AllArgsConstructor @NoArgsConstructor
|
||||||
|
@Getter @EqualsAndHashCode
|
||||||
|
public class Player {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UUID of the player
|
||||||
|
*/
|
||||||
|
private UUID uniqueId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The trimmed UUID of the player
|
||||||
|
*/
|
||||||
|
private String trimmedUniqueId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the player
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The skin of the player, null if the
|
||||||
|
* player does not have a skin
|
||||||
|
*/
|
||||||
|
private Skin skin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cape of the player, null if the
|
||||||
|
* player does not have a cape
|
||||||
|
*/
|
||||||
|
private Cape cape;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw properties of the player
|
||||||
|
*/
|
||||||
|
private MojangProfileToken.ProfileProperty[] rawProperties;
|
||||||
|
|
||||||
|
public Player(MojangProfileToken profile) {
|
||||||
|
this.uniqueId = UUIDUtils.addDashes(profile.getId());
|
||||||
|
this.trimmedUniqueId = UUIDUtils.removeDashes(this.uniqueId);
|
||||||
|
this.username = profile.getName();
|
||||||
|
this.rawProperties = profile.getProperties();
|
||||||
|
|
||||||
|
// Get the skin and cape
|
||||||
|
Tuple<Skin, Cape> skinAndCape = profile.getSkinAndCape();
|
||||||
|
if (skinAndCape != null) {
|
||||||
|
this.skin = skinAndCape.getLeft();
|
||||||
|
this.cape = skinAndCape.getRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package xyz.mcutils.backend.model.response;
|
||||||
|
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Getter @ToString @EqualsAndHashCode
|
||||||
|
public class ErrorResponse {
|
||||||
|
/**
|
||||||
|
* The status code of this error.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final HttpStatus status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTTP code of this error.
|
||||||
|
*/
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message of this error.
|
||||||
|
*/
|
||||||
|
@NonNull private final String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp this error occurred.
|
||||||
|
*/
|
||||||
|
@NonNull private final Date timestamp;
|
||||||
|
|
||||||
|
public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) {
|
||||||
|
this.status = status;
|
||||||
|
code = status.value();
|
||||||
|
this.message = message;
|
||||||
|
timestamp = new Date();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
package xyz.mcutils.backend.model.server;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Bedrock edition {@link MinecraftServer}.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Getter @ToString(callSuper = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
|
||||||
|
public final class BedrockMinecraftServer extends MinecraftServer {
|
||||||
|
/**
|
||||||
|
* The unique ID of this server.
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode.Include @NonNull private final String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The edition of this server.
|
||||||
|
*/
|
||||||
|
@NonNull private final Edition edition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version information of this server.
|
||||||
|
*/
|
||||||
|
@NonNull private final Version version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gamemode of this server.
|
||||||
|
*/
|
||||||
|
@NonNull private final GameMode gamemode;
|
||||||
|
|
||||||
|
private BedrockMinecraftServer(@NonNull String id, @NonNull String hostname, String ip, int port, @NonNull DNSRecord[] records,
|
||||||
|
@NonNull Edition edition, @NonNull Version version, @NonNull Players players, @NonNull MOTD motd,
|
||||||
|
@NonNull GameMode gamemode, GeoLocation location) {
|
||||||
|
super(hostname, ip, port, records, motd, players, location);
|
||||||
|
this.id = id;
|
||||||
|
this.edition = edition;
|
||||||
|
this.version = version;
|
||||||
|
this.gamemode = gamemode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Bedrock Minecraft server.
|
||||||
|
* <p>
|
||||||
|
* <a href="https://wiki.vg/Raknet_Protocol#Unconnected_Pong">Token Format</a>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param hostname the hostname of the server
|
||||||
|
* @param ip the IP address of the server
|
||||||
|
* @param port the port of the server
|
||||||
|
* @param token the status token
|
||||||
|
* @return the Bedrock Minecraft server
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static BedrockMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull String token) {
|
||||||
|
String[] split = token.split(";"); // Split the token
|
||||||
|
Edition edition = Edition.valueOf(split[0]);
|
||||||
|
Version version = new Version(Integer.parseInt(split[2]), split[3]);
|
||||||
|
Players players = new Players(Integer.parseInt(split[4]), Integer.parseInt(split[5]), null);
|
||||||
|
MOTD motd = MOTD.create(hostname, Platform.BEDROCK, split[1] + "\n" + split[7]);
|
||||||
|
GameMode gameMode = new GameMode(split[8], split.length > 9 ? Integer.parseInt(split[9]) : -1);
|
||||||
|
return new BedrockMinecraftServer(
|
||||||
|
split[6],
|
||||||
|
hostname,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
records,
|
||||||
|
edition,
|
||||||
|
version,
|
||||||
|
players,
|
||||||
|
motd,
|
||||||
|
gameMode,
|
||||||
|
location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The edition of a Bedrock server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public enum Edition {
|
||||||
|
/**
|
||||||
|
* Minecraft: Pocket Edition.
|
||||||
|
*/
|
||||||
|
MCPE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minecraft: Education Edition.
|
||||||
|
*/
|
||||||
|
MCEE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version information for a server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public static class Version {
|
||||||
|
/**
|
||||||
|
* The protocol version of the server.
|
||||||
|
*/
|
||||||
|
private final int protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version name of the server.
|
||||||
|
*/
|
||||||
|
@NonNull private final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gamemode of a server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public static class GameMode {
|
||||||
|
/**
|
||||||
|
* The name of this gamemode.
|
||||||
|
*/
|
||||||
|
@NonNull private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The numeric of this gamemode.
|
||||||
|
*/
|
||||||
|
private final int numericId;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,272 @@
|
|||||||
|
package xyz.mcutils.backend.model.server;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import lombok.*;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.chat.ComponentSerializer;
|
||||||
|
import xyz.mcutils.backend.Main;
|
||||||
|
import xyz.mcutils.backend.common.JavaMinecraftVersion;
|
||||||
|
import xyz.mcutils.backend.common.ServerUtils;
|
||||||
|
import xyz.mcutils.backend.config.Config;
|
||||||
|
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||||
|
import xyz.mcutils.backend.model.token.JavaServerStatusToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@Setter @Getter @EqualsAndHashCode(callSuper = false)
|
||||||
|
public final class JavaMinecraftServer extends MinecraftServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the server.
|
||||||
|
*/
|
||||||
|
@NonNull private final Version version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The favicon of the server.
|
||||||
|
*/
|
||||||
|
private Favicon favicon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mods running on this server.
|
||||||
|
*/
|
||||||
|
private ForgeModInfo modInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mods running on this server.
|
||||||
|
* <p>
|
||||||
|
* This is only used for servers
|
||||||
|
* running 1.13 and above.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private ForgeData forgeData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server prevents chat reports.
|
||||||
|
*/
|
||||||
|
private boolean preventsChatReports;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server enforces secure chat.
|
||||||
|
*/
|
||||||
|
private boolean enforcesSecureChat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server has previews chat enabled.
|
||||||
|
* <p>
|
||||||
|
* Chat Preview sends chat messages to the server as they are typed, even before they're sent.
|
||||||
|
* <a href="https://www.minecraft.net/es-mx/article/minecraft-snapshot-22w19a">More information</a>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private boolean previewsChat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mojang blocked status for the server.
|
||||||
|
*/
|
||||||
|
private boolean mojangBlocked;
|
||||||
|
|
||||||
|
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, Players players, GeoLocation location,
|
||||||
|
DNSRecord[] records, @NonNull Version version, Favicon favicon, ForgeModInfo modInfo,
|
||||||
|
ForgeData forgeData, boolean preventsChatReports, boolean enforcesSecureChat, boolean previewsChat) {
|
||||||
|
super(hostname, ip, port, records, motd, players, location);
|
||||||
|
this.version = version;
|
||||||
|
this.favicon = favicon;
|
||||||
|
this.modInfo = modInfo;
|
||||||
|
this.forgeData = forgeData;
|
||||||
|
this.preventsChatReports = preventsChatReports;
|
||||||
|
this.enforcesSecureChat = enforcesSecureChat;
|
||||||
|
this.previewsChat = previewsChat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Java Minecraft server.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname of the server
|
||||||
|
* @param ip the IP address of the server
|
||||||
|
* @param port the port of the server
|
||||||
|
* @param token the status token
|
||||||
|
* @return the Java Minecraft server
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, DNSRecord[] records, GeoLocation location, @NonNull JavaServerStatusToken token) {
|
||||||
|
String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null;
|
||||||
|
if (motdString == null) { // Not a string motd, convert from Json
|
||||||
|
motdString = new TextComponent(ComponentSerializer.parse(Main.GSON.toJson(token.getDescription()))).toLegacyText();
|
||||||
|
}
|
||||||
|
return new JavaMinecraftServer(
|
||||||
|
hostname,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
MinecraftServer.MOTD.create(hostname, Platform.JAVA, motdString),
|
||||||
|
token.getPlayers(),
|
||||||
|
location,
|
||||||
|
records,
|
||||||
|
token.getVersion().detailedCopy(),
|
||||||
|
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port)),
|
||||||
|
token.getModInfo(),
|
||||||
|
token.getForgeData(),
|
||||||
|
token.isPreventsChatReports(),
|
||||||
|
token.isEnforcesSecureChat(),
|
||||||
|
token.isPreviewsChat()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class Version {
|
||||||
|
/**
|
||||||
|
* The version name of the server.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server platform.
|
||||||
|
*/
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The protocol version.
|
||||||
|
*/
|
||||||
|
private final int protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the protocol, null if not found.
|
||||||
|
*/
|
||||||
|
private final String protocolName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a more detailed
|
||||||
|
* copy of this object.
|
||||||
|
*
|
||||||
|
* @return the detailed copy
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Version detailedCopy() {
|
||||||
|
String platform = null;
|
||||||
|
if (name.contains(" ")) { // Parse the server platform
|
||||||
|
String[] split = name.split(" ");
|
||||||
|
if (split.length == 2) {
|
||||||
|
platform = split[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JavaMinecraftVersion minecraftVersion = JavaMinecraftVersion.byProtocol(protocol);
|
||||||
|
return new Version(name, platform, protocol, minecraftVersion == null ? null : minecraftVersion.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter @AllArgsConstructor
|
||||||
|
public static class Favicon {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw base64 of the favicon.
|
||||||
|
*/
|
||||||
|
private final String base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url to the favicon.
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new favicon for a server.
|
||||||
|
*
|
||||||
|
* @param base64 the base64 of the favicon
|
||||||
|
* @param address the address of the server
|
||||||
|
* @return the new favicon
|
||||||
|
*/
|
||||||
|
public static Favicon create(String base64, @NonNull String address) {
|
||||||
|
if (base64 == null) { // The server doesn't have a favicon
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Favicon(base64, Config.INSTANCE.getWebPublicUrl() + "/server/icon/%s".formatted(address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forge mod information for a server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public static class ForgeModInfo {
|
||||||
|
/**
|
||||||
|
* The type of modded server this is.
|
||||||
|
*/
|
||||||
|
@NonNull private final String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of mods on this server, null or empty if none.
|
||||||
|
*/
|
||||||
|
private final ForgeMod[] modList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A forge mod for a server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
private static class ForgeMod {
|
||||||
|
/**
|
||||||
|
* The id of this mod.
|
||||||
|
*/
|
||||||
|
@NonNull @SerializedName("modid") private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of this mod.
|
||||||
|
*/
|
||||||
|
private final String version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class ForgeData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of mod channels on this server, null or empty if none.
|
||||||
|
*/
|
||||||
|
private final Channel[] channels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of mods on this server, null or empty if none.
|
||||||
|
*/
|
||||||
|
private final Mod[] mods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the mod list is truncated.
|
||||||
|
*/
|
||||||
|
private final boolean truncated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the FML network.
|
||||||
|
*/
|
||||||
|
private final int fmlNetworkVersion;
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class Channel {
|
||||||
|
/**
|
||||||
|
* The id of this mod channel.
|
||||||
|
*/
|
||||||
|
@NonNull @SerializedName("res") private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of this mod channel.
|
||||||
|
*/
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this mod channel is required to join.
|
||||||
|
*/
|
||||||
|
private boolean required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class Mod {
|
||||||
|
/**
|
||||||
|
* The id of this mod.
|
||||||
|
*/
|
||||||
|
@NonNull @SerializedName("modId") private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of this mod.
|
||||||
|
*/
|
||||||
|
@SerializedName("modmarker") private final String version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,214 @@
|
|||||||
|
package xyz.mcutils.backend.model.server;
|
||||||
|
|
||||||
|
import com.maxmind.geoip2.model.CityResponse;
|
||||||
|
import io.micrometer.common.lang.NonNull;
|
||||||
|
import lombok.*;
|
||||||
|
import xyz.mcutils.backend.common.ColorUtils;
|
||||||
|
import xyz.mcutils.backend.config.Config;
|
||||||
|
import xyz.mcutils.backend.model.dns.DNSRecord;
|
||||||
|
import xyz.mcutils.backend.service.pinger.MinecraftServerPinger;
|
||||||
|
import xyz.mcutils.backend.service.pinger.impl.BedrockMinecraftServerPinger;
|
||||||
|
import xyz.mcutils.backend.service.pinger.impl.JavaMinecraftServerPinger;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter @Setter @EqualsAndHashCode
|
||||||
|
public class MinecraftServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hostname of the server.
|
||||||
|
*/
|
||||||
|
private final String hostname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The IP address of the server.
|
||||||
|
*/
|
||||||
|
private final String ip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port of the server.
|
||||||
|
*/
|
||||||
|
private final int port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DNS records for the server.
|
||||||
|
*/
|
||||||
|
private final DNSRecord[] records;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The motd for the server.
|
||||||
|
*/
|
||||||
|
private final MOTD motd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The players on the server.
|
||||||
|
*/
|
||||||
|
private final Players players;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the server.
|
||||||
|
*/
|
||||||
|
private final GeoLocation location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A platform a Minecraft
|
||||||
|
* server can operate on.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public enum Platform {
|
||||||
|
/**
|
||||||
|
* The Java edition of Minecraft.
|
||||||
|
*/
|
||||||
|
JAVA(new JavaMinecraftServerPinger(), 25565),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bedrock edition of Minecraft.
|
||||||
|
*/
|
||||||
|
BEDROCK(new BedrockMinecraftServerPinger(), 19132);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server pinger for this platform.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final MinecraftServerPinger<?> pinger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default server port for this platform.
|
||||||
|
*/
|
||||||
|
private final int defaultPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class MOTD {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw motd lines
|
||||||
|
*/
|
||||||
|
private final String[] raw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The clean motd lines
|
||||||
|
*/
|
||||||
|
private final String[] clean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html motd lines
|
||||||
|
*/
|
||||||
|
private final String[] html;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the server preview image.
|
||||||
|
*/
|
||||||
|
private final String preview;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new MOTD from a raw string.
|
||||||
|
*
|
||||||
|
* @param raw the raw motd string
|
||||||
|
* @return the new motd
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static MOTD create(@NonNull String hostname, @NonNull Platform platform, @NonNull String raw) {
|
||||||
|
String[] rawLines = raw.split("\n"); // The raw lines
|
||||||
|
return new MOTD(
|
||||||
|
rawLines,
|
||||||
|
Arrays.stream(rawLines).map(ColorUtils::stripColor).toArray(String[]::new),
|
||||||
|
Arrays.stream(rawLines).map(ColorUtils::toHTML).toArray(String[]::new),
|
||||||
|
Config.INSTANCE.getWebPublicUrl() + "/server/%s/preview/%s".formatted(
|
||||||
|
platform.name().toLowerCase(),hostname)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player count data for a server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class Players {
|
||||||
|
/**
|
||||||
|
* The online players on this server.
|
||||||
|
*/
|
||||||
|
private final int online;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum allowed players on this server.
|
||||||
|
*/
|
||||||
|
private final int max;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sample of players on this server, null or empty if no sample.
|
||||||
|
*/
|
||||||
|
private final Sample[] sample;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sample player.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public static class Sample {
|
||||||
|
/**
|
||||||
|
* The unique id of this player.
|
||||||
|
*/
|
||||||
|
@NonNull private final UUID id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this player.
|
||||||
|
*/
|
||||||
|
@NonNull private final String name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The location of the server.
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class GeoLocation {
|
||||||
|
/**
|
||||||
|
* The country of the server.
|
||||||
|
*/
|
||||||
|
private final String country;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The region of the server.
|
||||||
|
*/
|
||||||
|
private final String region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The city of the server.
|
||||||
|
*/
|
||||||
|
private final String city;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The latitude of the server.
|
||||||
|
*/
|
||||||
|
private final double latitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The longitude of the server.
|
||||||
|
*/
|
||||||
|
private final double longitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the location of the server from Maxmind.
|
||||||
|
*
|
||||||
|
* @param response the response from Maxmind
|
||||||
|
* @return the location of the server
|
||||||
|
*/
|
||||||
|
public static GeoLocation fromMaxMind(CityResponse response) {
|
||||||
|
if (response == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new GeoLocation(
|
||||||
|
response.getCountry().getName(),
|
||||||
|
response.getMostSpecificSubdivision().getName(),
|
||||||
|
response.getCity().getName(),
|
||||||
|
response.getLocation().getLatitude(),
|
||||||
|
response.getLocation().getLongitude()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
207
src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java
Normal file
207
src/main/java/xyz/mcutils/backend/model/skin/ISkinPart.java
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package xyz.mcutils.backend.model.skin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import xyz.mcutils.backend.common.renderer.SkinRenderer;
|
||||||
|
import xyz.mcutils.backend.common.renderer.impl.skin.BodyRenderer;
|
||||||
|
import xyz.mcutils.backend.common.renderer.impl.skin.IsometricHeadRenderer;
|
||||||
|
import xyz.mcutils.backend.common.renderer.impl.skin.SquareRenderer;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public interface ISkinPart {
|
||||||
|
Enum<?>[][] TYPES = { Vanilla.values(), Custom.values() };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the part.
|
||||||
|
*
|
||||||
|
* @return the part name
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should this part be hidden from the
|
||||||
|
* player skin part urls list?
|
||||||
|
*
|
||||||
|
* @return whether this part should be hidden
|
||||||
|
*/
|
||||||
|
boolean hidden();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the skin part for the skin.
|
||||||
|
*
|
||||||
|
* @param skin the skin
|
||||||
|
* @param renderOverlays should the overlays be rendered
|
||||||
|
* @param size the size of the part
|
||||||
|
* @return the rendered skin part
|
||||||
|
*/
|
||||||
|
BufferedImage render(Skin skin, boolean renderOverlays, int size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a skin part by the given name.
|
||||||
|
*
|
||||||
|
* @param name the name of the part
|
||||||
|
* @return the part, null if none
|
||||||
|
*/
|
||||||
|
static ISkinPart getByName(String name) {
|
||||||
|
name = name.toUpperCase();
|
||||||
|
for (Enum<?>[] type : TYPES) {
|
||||||
|
for (Enum<?> part : type) {
|
||||||
|
if (!part.name().equals(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return (ISkinPart) part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The vanilla skin parts.
|
||||||
|
* <p>
|
||||||
|
* <a href="https://cdn.fascinated.cc/sXwEKAxm.png">Skin Format</a>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
enum Vanilla implements ISkinPart {
|
||||||
|
// Overlays
|
||||||
|
HEAD_OVERLAY_TOP(true, new Coordinates(40, 0), 8, 8),
|
||||||
|
HEAD_OVERLAY_FACE(true, new Coordinates(40, 8), 8, 8),
|
||||||
|
HEAD_OVERLAY_LEFT(true, new Coordinates(48, 8), 8, 8),
|
||||||
|
|
||||||
|
// Head
|
||||||
|
HEAD_TOP(true, new Coordinates(8, 0), 8, 8, HEAD_OVERLAY_TOP),
|
||||||
|
FACE(false, new Coordinates(8, 8), 8, 8, HEAD_OVERLAY_FACE),
|
||||||
|
HEAD_LEFT(true, new Coordinates(0, 8), 8, 8, HEAD_OVERLAY_LEFT),
|
||||||
|
HEAD_RIGHT(true, new Coordinates(16, 8), 8, 8),
|
||||||
|
HEAD_BOTTOM(true, new Coordinates(16, 0), 8, 8),
|
||||||
|
HEAD_BACK(true, new Coordinates(24, 8), 8, 8),
|
||||||
|
|
||||||
|
// Body
|
||||||
|
BODY_FRONT(true, new Coordinates(20, 20), 8, 12),
|
||||||
|
|
||||||
|
// Arms
|
||||||
|
LEFT_ARM_TOP(true, new Coordinates(36, 48), 4, 4),
|
||||||
|
RIGHT_ARM_TOP(true, new Coordinates(44, 16), 4, 4),
|
||||||
|
|
||||||
|
LEFT_ARM_FRONT(true, new Coordinates(44, 20), 4, 12),
|
||||||
|
RIGHT_ARM_FRONT(true, new Coordinates(36, 52), new LegacyCoordinates(44, 20, true), 4, 12),
|
||||||
|
|
||||||
|
// Legs
|
||||||
|
LEFT_LEG_FRONT(true, new Coordinates(4, 20), 4, 12), // Front
|
||||||
|
RIGHT_LEG_FRONT(true, new Coordinates(20, 52), new LegacyCoordinates(4, 20, true), 4, 12); // Front
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should this part be hidden from the
|
||||||
|
* player skin part urls list?
|
||||||
|
*/
|
||||||
|
private final boolean hidden;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The coordinates of the part.
|
||||||
|
*/
|
||||||
|
private final Coordinates coordinates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The legacy coordinates of the part.
|
||||||
|
*/
|
||||||
|
private final LegacyCoordinates legacyCoordinates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width and height of the part.
|
||||||
|
*/
|
||||||
|
private final int width, height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The overlays of the part.
|
||||||
|
*/
|
||||||
|
private final Vanilla[] overlays;
|
||||||
|
|
||||||
|
Vanilla(boolean hidden, Coordinates coordinates, int width, int height, Vanilla... overlays) {
|
||||||
|
this(hidden, coordinates, null, width, height, overlays);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vanilla(boolean hidden, Coordinates coordinates, LegacyCoordinates legacyCoordinates, int width, int height, Vanilla... overlays) {
|
||||||
|
this.hidden = hidden;
|
||||||
|
this.coordinates = coordinates;
|
||||||
|
this.legacyCoordinates = legacyCoordinates;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.overlays = overlays;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hidden() {
|
||||||
|
return this.isHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(Skin skin, boolean renderOverlays, int size) {
|
||||||
|
return SquareRenderer.INSTANCE.render(skin, this, renderOverlays, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this part a front arm?
|
||||||
|
*
|
||||||
|
* @return whether this part is a front arm
|
||||||
|
*/
|
||||||
|
public boolean isFrontArm() {
|
||||||
|
return this == LEFT_ARM_FRONT || this == RIGHT_ARM_FRONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this part have legacy coordinates?
|
||||||
|
*
|
||||||
|
* @return whether this part has legacy coordinates
|
||||||
|
*/
|
||||||
|
public boolean hasLegacyCoordinates() {
|
||||||
|
return legacyCoordinates != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
public static class Coordinates {
|
||||||
|
/**
|
||||||
|
* The X and Y position of the part.
|
||||||
|
*/
|
||||||
|
private final int x, y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class LegacyCoordinates extends Coordinates {
|
||||||
|
/**
|
||||||
|
* Should the part be flipped horizontally?
|
||||||
|
*/
|
||||||
|
private final boolean flipped;
|
||||||
|
|
||||||
|
public LegacyCoordinates(int x, int y) {
|
||||||
|
this(x, y, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegacyCoordinates(int x, int y, boolean flipped) {
|
||||||
|
super(x, y);
|
||||||
|
this.flipped = flipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor @Getter
|
||||||
|
enum Custom implements ISkinPart {
|
||||||
|
HEAD(IsometricHeadRenderer.INSTANCE),
|
||||||
|
BODY(BodyRenderer.INSTANCE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The renderer to use for this part
|
||||||
|
*/
|
||||||
|
private final SkinRenderer<Custom> renderer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hidden() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedImage render(Skin skin, boolean renderOverlays, int size) {
|
||||||
|
return renderer.render(skin, this, renderOverlays, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
src/main/java/xyz/mcutils/backend/model/skin/Skin.java
Normal file
108
src/main/java/xyz/mcutils/backend/model/skin/Skin.java
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package xyz.mcutils.backend.model.skin;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import xyz.mcutils.backend.common.EnumUtils;
|
||||||
|
import xyz.mcutils.backend.common.PlayerUtils;
|
||||||
|
import xyz.mcutils.backend.config.Config;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@AllArgsConstructor @NoArgsConstructor
|
||||||
|
@Getter @Log4j2(topic = "Skin") @EqualsAndHashCode
|
||||||
|
public class Skin {
|
||||||
|
/**
|
||||||
|
* The URL for the skin
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model for the skin
|
||||||
|
*/
|
||||||
|
private Model model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The legacy status of the skin
|
||||||
|
*/
|
||||||
|
private boolean legacy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The skin image for the skin
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
private byte[] skinImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The part URLs of the skin
|
||||||
|
*/
|
||||||
|
@JsonProperty("parts")
|
||||||
|
private Map<String, String> partUrls = new HashMap<>();
|
||||||
|
|
||||||
|
public Skin(String url, Model model) {
|
||||||
|
this.url = url;
|
||||||
|
this.model = model;
|
||||||
|
|
||||||
|
this.skinImage = PlayerUtils.getSkinImage(url);
|
||||||
|
if (this.skinImage != null) {
|
||||||
|
try {
|
||||||
|
BufferedImage image = ImageIO.read(new ByteArrayInputStream(this.skinImage));
|
||||||
|
this.legacy = image.getWidth() == 64 && image.getHeight() == 32;
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the skin from a {@link JsonObject}.
|
||||||
|
*
|
||||||
|
* @param json the JSON object
|
||||||
|
* @return the skin
|
||||||
|
*/
|
||||||
|
public static Skin fromJson(JsonObject json) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String url = json.get("url").getAsString();
|
||||||
|
JsonObject metadata = json.getAsJsonObject("metadata");
|
||||||
|
return new Skin(
|
||||||
|
url,
|
||||||
|
EnumUtils.getEnumConstant(Model.class, metadata != null ? metadata.get("model").getAsString().toUpperCase() : "DEFAULT")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the part URLs for the skin.
|
||||||
|
*
|
||||||
|
* @param playerUuid the player's UUID
|
||||||
|
*/
|
||||||
|
public Skin populatePartUrls(String playerUuid) {
|
||||||
|
for (Enum<?>[] type : ISkinPart.TYPES) {
|
||||||
|
for (Enum<?> part : type) {
|
||||||
|
ISkinPart skinPart = (ISkinPart) part;
|
||||||
|
if (skinPart.hidden()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String partName = part.name().toLowerCase();
|
||||||
|
this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model of the skin.
|
||||||
|
*/
|
||||||
|
public enum Model {
|
||||||
|
DEFAULT,
|
||||||
|
SLIM
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package xyz.mcutils.backend.model.token;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import xyz.mcutils.backend.model.server.JavaMinecraftServer;
|
||||||
|
import xyz.mcutils.backend.model.server.MinecraftServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor @Getter @ToString
|
||||||
|
public final class JavaServerStatusToken {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the server.
|
||||||
|
*/
|
||||||
|
private final JavaMinecraftServer.Version version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The players on the server.
|
||||||
|
*/
|
||||||
|
private final MinecraftServer.Players players;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mods running on this server.
|
||||||
|
*/
|
||||||
|
@SerializedName("modinfo")
|
||||||
|
private JavaMinecraftServer.ForgeModInfo modInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mods running on this server.
|
||||||
|
* <p>
|
||||||
|
* This is only used for servers
|
||||||
|
* running 1.13 and above.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private JavaMinecraftServer.ForgeData forgeData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The motd of the server.
|
||||||
|
*/
|
||||||
|
private final Object description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The favicon of the server.
|
||||||
|
*/
|
||||||
|
private final String favicon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server prevents chat reports.
|
||||||
|
*/
|
||||||
|
private boolean preventsChatReports;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server enforces secure chat.
|
||||||
|
*/
|
||||||
|
private boolean enforcesSecureChat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the server has previews chat enabled.
|
||||||
|
* <p>
|
||||||
|
* Chat Preview sends chat messages to the server as they are typed, even before they're sent.
|
||||||
|
* <a href="https://www.minecraft.net/es-mx/article/minecraft-snapshot-22w19a">More information</a>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private boolean previewsChat;
|
||||||
|
}
|
@ -1,21 +1,20 @@
|
|||||||
package cc.fascinated.model.mojang;
|
package xyz.mcutils.backend.model.token;
|
||||||
|
|
||||||
import cc.fascinated.Main;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import cc.fascinated.common.Tuple;
|
|
||||||
import cc.fascinated.common.UUIDUtils;
|
|
||||||
import cc.fascinated.model.player.Cape;
|
|
||||||
import cc.fascinated.model.player.Skin;
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import xyz.mcutils.backend.Main;
|
||||||
|
import xyz.mcutils.backend.common.Tuple;
|
||||||
|
import xyz.mcutils.backend.common.UUIDUtils;
|
||||||
|
import xyz.mcutils.backend.model.player.Cape;
|
||||||
|
import xyz.mcutils.backend.model.skin.Skin;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Getter @NoArgsConstructor
|
@Getter @NoArgsConstructor @AllArgsConstructor
|
||||||
public class MojangProfile {
|
public class MojangProfileToken {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the player.
|
* The UUID of the player.
|
||||||
@ -30,7 +29,7 @@ public class MojangProfile {
|
|||||||
/**
|
/**
|
||||||
* The properties of the player.
|
* The properties of the player.
|
||||||
*/
|
*/
|
||||||
private final List<ProfileProperty> properties = new ArrayList<>();
|
private ProfileProperty[] properties = new ProfileProperty[0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the skin and cape of the player.
|
* Get the skin and cape of the player.
|
||||||
@ -42,10 +41,7 @@ public class MojangProfile {
|
|||||||
if (textureProperty == null) {
|
if (textureProperty == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
JsonObject texturesJson = textureProperty.getDecodedValue().getAsJsonObject("textures"); // Parse the decoded JSON and get the texture object
|
||||||
JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property
|
|
||||||
JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object
|
|
||||||
|
|
||||||
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
|
return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()),
|
||||||
Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
|
Cape.fromJson(texturesJson.getAsJsonObject("CAPE")));
|
||||||
}
|
}
|
||||||
@ -56,7 +52,7 @@ public class MojangProfile {
|
|||||||
* @return the formatted UUID
|
* @return the formatted UUID
|
||||||
*/
|
*/
|
||||||
public String getFormattedUuid() {
|
public String getFormattedUuid() {
|
||||||
return id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id;
|
return id.length() == 32 ? UUIDUtils.addDashes(id).toString() : id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,7 +69,7 @@ public class MojangProfile {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter @AllArgsConstructor
|
@Getter @NoArgsConstructor
|
||||||
public static class ProfileProperty {
|
public static class ProfileProperty {
|
||||||
/**
|
/**
|
||||||
* The name of the property.
|
* The name of the property.
|
||||||
@ -95,8 +91,9 @@ public class MojangProfile {
|
|||||||
*
|
*
|
||||||
* @return the decoded value
|
* @return the decoded value
|
||||||
*/
|
*/
|
||||||
public String getDecodedValue() {
|
@JsonIgnore
|
||||||
return new String(Base64.getDecoder().decode(this.value));
|
public JsonObject getDecodedValue() {
|
||||||
|
return Main.GSON.fromJson(new String(Base64.getDecoder().decode(this.value)), JsonObject.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,20 +1,23 @@
|
|||||||
package cc.fascinated.model.mojang;
|
package xyz.mcutils.backend.model.token;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Getter @NoArgsConstructor
|
@Getter @NoArgsConstructor
|
||||||
public class MojangUsernameToUuid {
|
public class MojangUsernameToUuidToken {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the player.
|
* The UUID of the player.
|
||||||
*/
|
*/
|
||||||
private String id;
|
@JsonProperty("id")
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the player.
|
* The name of the player.
|
||||||
*/
|
*/
|
||||||
private String name;
|
@JsonProperty("name")
|
||||||
|
private String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the profile is valid.
|
* Check if the profile is valid.
|
||||||
@ -22,6 +25,6 @@ public class MojangUsernameToUuid {
|
|||||||
* @return if the profile is valid
|
* @return if the profile is valid
|
||||||
*/
|
*/
|
||||||
public boolean isValid() {
|
public boolean isValid() {
|
||||||
return id != null && name != null;
|
return uuid != null && username != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package xyz.mcutils.backend.repository.mongo;
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
import xyz.mcutils.backend.service.metric.Metric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repository for {@link Metric}s.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public interface MetricsRepository extends MongoRepository<Metric<?>, String> { }
|
11
src/main/java/xyz/mcutils/backend/repository/redis/MinecraftServerCacheRepository.java
Normal file
11
src/main/java/xyz/mcutils/backend/repository/redis/MinecraftServerCacheRepository.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package xyz.mcutils.backend.repository.redis;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache repository for {@link CachedMinecraftServer}'s.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public interface MinecraftServerCacheRepository extends CrudRepository<CachedMinecraftServer, String> { }
|
@ -0,0 +1,13 @@
|
|||||||
|
package xyz.mcutils.backend.repository.redis;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
import xyz.mcutils.backend.model.cache.CachedPlayer;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache repository for {@link CachedPlayer}'s.
|
||||||
|
*
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public interface PlayerCacheRepository extends CrudRepository<CachedPlayer, UUID> { }
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user