Compare commits
346
Commits
dev-upstream
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c5b8bcc31
|
||
|
|
31a7b7c087
|
||
|
|
eff7e517a6 | ||
|
|
2a34066187 | ||
|
|
35bbe9ecba | ||
|
|
7197787ad3 | ||
|
|
f93401ff26 | ||
|
|
6169116dc7 | ||
|
|
3b13e277f7 | ||
|
|
a8bb0f159a | ||
|
|
aa76e82b16 | ||
|
|
9c6f94c10b | ||
|
|
fec77fba15 | ||
|
|
3a5625508b | ||
|
|
cb864f7eba | ||
|
|
9d50254dec | ||
|
|
2bfd3c48b6 | ||
|
|
c6df9f029a | ||
|
|
cc6e38c7f2 | ||
|
|
c22f4f0efe | ||
|
|
71cd756337 | ||
|
|
4a5543da2a | ||
|
|
5a8682dc26 | ||
|
|
0ed4e78afd | ||
|
|
6b049d98b0 | ||
|
|
791e0575a9 | ||
|
|
35faf08cfa | ||
|
|
622c23a729 | ||
|
|
2eba6ec2ab | ||
|
|
8783dd0683 | ||
|
|
e861daa8c1 | ||
|
|
c970c1af6f | ||
|
|
921c009a59 | ||
|
|
ea0c2736e1 | ||
|
|
25411ba54f | ||
|
|
715013d90f | ||
|
|
a59ff6e625 | ||
|
|
bf609a1964 | ||
|
|
af5446f33e | ||
|
|
da3ceeca55 | ||
|
|
f665d1e13e | ||
|
|
8e52dae5b2 | ||
|
|
01188bc79f | ||
|
|
664d3899d7 | ||
|
|
60f0b37adc | ||
|
|
511c634c41 | ||
|
|
a3b5999de3 | ||
|
|
1666d815b2 | ||
|
|
b447154d52 | ||
|
|
73e862a782 | ||
|
|
6d1ab1acbe | ||
|
|
151eb58fb6 | ||
|
|
1beedd84d0 | ||
|
|
bc3175faed | ||
|
|
58f61073c6 | ||
|
|
26c72b5490 | ||
|
|
991e70e676 | ||
|
|
c8519f4d74 | ||
|
|
c9fef78eac | ||
|
|
0ce7104f72 | ||
|
|
74e7c3930c | ||
|
|
31ab0aa7ff | ||
|
|
fc34b3402e | ||
|
|
db6440d87c | ||
|
|
0119309ffb | ||
|
|
f31fcc437f | ||
|
|
ab4d9d13c9 | ||
|
|
052454fee0 | ||
|
|
0f0b9f2cec | ||
|
|
2eb1fff556 | ||
|
|
b4f8786683 | ||
|
|
5c7f314071 | ||
|
|
73ab2388dc | ||
|
|
7c129185c5 | ||
|
|
ac5bd598f0 | ||
|
|
98f23966db | ||
|
|
0803d2800f | ||
|
|
83dab3cb8c | ||
|
|
768bc20e14 | ||
|
|
f3bd42ff2f | ||
|
|
a56fd79d29 | ||
|
|
e1b2487396 | ||
|
|
f19499205f | ||
|
|
39e8cbddcb | ||
|
|
42c3129060 | ||
|
|
a6a2577e07 | ||
|
|
47ff08d62a | ||
|
|
619e50dd6a | ||
|
|
98cbf4adfc | ||
|
|
185bb72f10 | ||
|
|
7b5949dba6 | ||
|
|
0be787c1e2 | ||
|
|
74ee5d8c7e | ||
|
|
2d98d4cbff | ||
|
|
c2bacd2f5c | ||
|
|
164d073861 | ||
|
|
77c4dde70f | ||
|
|
3745648821 | ||
|
|
b339a53dc8 | ||
|
|
3895b35941 | ||
|
|
ac5175b63c | ||
|
|
8e61e44012 | ||
|
|
233a1e04d3 | ||
|
|
170caff375 | ||
|
|
a2cc42a0a7 | ||
|
|
ae76933a5d | ||
|
|
992ab4002a | ||
|
|
bb5b39810c | ||
|
|
dc7edf4361 | ||
|
|
702618482d | ||
|
|
a35b208df2 | ||
|
|
a51e9ac32e | ||
|
|
f4df09802b | ||
|
|
0a08b76d35 | ||
|
|
f7e572ef02 | ||
|
|
40d60b62a2 | ||
|
|
92f0cf7729 | ||
|
|
b34417d1c8 | ||
|
|
b9cc9f1f7f | ||
|
|
0c7b6effde | ||
|
|
f2807153b5 | ||
|
|
4a460f28cd | ||
|
|
fb62b8d99d | ||
|
|
e056cec07c | ||
|
|
e18a58e4f2 | ||
|
|
36a2bb5165 | ||
|
|
7a898acfd9 | ||
|
|
ac5d3307ed | ||
|
|
a652147e2c | ||
|
|
f7c4e179b7 | ||
|
|
3991e8f09a | ||
|
|
44239355b9 | ||
|
|
0b6af2aacd | ||
|
|
85c6a2cf14 | ||
|
|
ca937a86d0 | ||
|
|
8a2f85fadc | ||
|
|
24b32f2eca | ||
|
|
7bfe5c584f | ||
|
|
e904f44a5f | ||
|
|
9797780d81 | ||
|
|
9da919e910 | ||
|
|
fe395f2cbd | ||
|
|
581bef78fe | ||
|
|
35e19c56b1 | ||
|
|
e2514fd229 | ||
|
|
1fceee5319 | ||
|
|
07b34d4744 | ||
|
|
b8ac667e5f | ||
|
|
a467a06df4 | ||
|
|
50bde1375a | ||
|
|
6ce4469db9 | ||
|
|
d63e4a0c29 | ||
|
|
e5b70523bc | ||
|
|
cf0e5decd7 | ||
|
|
cbe1c3c46e | ||
|
|
1db1e02c8b | ||
|
|
7a9fee6e2d | ||
|
|
addb077133 | ||
|
|
1fcbd33a62 | ||
|
|
f6e2498722 | ||
|
|
e6367842ed | ||
|
|
380174dade | ||
|
|
dab6b7a85b | ||
|
|
4124c3ade3 | ||
|
|
9abe7259a2 | ||
|
|
79656875c3 | ||
|
|
bc6872d56f | ||
|
|
fb24f31146 | ||
|
|
4df4b6c637 | ||
|
|
c15caa32e3 | ||
|
|
833559eec9 | ||
|
|
c3e6f06b9f | ||
|
|
af39cda3b8 | ||
|
|
98d39570d8 | ||
|
|
be88498fe5 | ||
|
|
1250cc1abc | ||
|
|
9b14df0881 | ||
|
|
11b2c3ff41 | ||
|
|
b842426448 | ||
|
|
3f789ecd25 | ||
|
|
4cacf2f4af | ||
|
|
3d8545f7f5 | ||
|
|
f2ac1b961e | ||
|
|
3474351e9d | ||
|
|
c66057c601 | ||
|
|
c6342981ee | ||
|
|
5ce4a98434 | ||
|
|
80d2017f4d | ||
|
|
309044ec67 | ||
|
|
136e1073af | ||
|
|
1ecb17c0ed | ||
|
|
c011c10637 | ||
|
|
098a2aa9bb | ||
|
|
a5a469bc23 | ||
|
|
1d887ab9d3 | ||
|
|
a7a8b2e5e2 | ||
|
|
be6da424c0 | ||
|
|
04da1ad403 | ||
|
|
216d7e437c | ||
|
|
52fb9038ac | ||
|
|
715f74a746 | ||
|
|
0ec8906310 | ||
|
|
be2d955872 | ||
|
|
6cfd096e5e | ||
|
|
37bc75c9c8 | ||
|
|
c59e147e6d | ||
|
|
ae3df3bf98 | ||
|
|
71bda96939 | ||
|
|
331807fdf3 | ||
|
|
70aac14038 | ||
|
|
4fe39867c6 | ||
|
|
50a7b75898 | ||
|
|
90d9ac81a6 | ||
|
|
8d1f0ad288 | ||
|
|
6d2253d3d4 | ||
|
|
033ba3fafa | ||
|
|
51d4f63850 | ||
|
|
10877e1a0a | ||
|
|
aa3721a60c | ||
|
|
4ecd3418d2 | ||
|
|
aa505c87bc | ||
|
|
941b9da0a1 | ||
|
|
55697f267f | ||
|
|
42c0dad6a8 | ||
|
|
eb150040b8 | ||
|
|
9c5f4f4054 | ||
|
|
d69814d1af | ||
|
|
0e324d5308 | ||
|
|
9f981aaebb | ||
|
|
68f580ef8f | ||
|
|
c338222583 | ||
|
|
495f4346d4 | ||
|
|
ec43afa356 | ||
|
|
55c7e61c7c | ||
|
|
d798768768 | ||
|
|
25b77fee08 | ||
|
|
390d827354 | ||
|
|
38f641aea5 | ||
|
|
1a95a1d38c | ||
|
|
8207e7e859 | ||
|
|
8301b50368 | ||
|
|
538b2bf431 | ||
|
|
808514541f | ||
|
|
5d7b5b7277 | ||
|
|
fac3a55086 | ||
|
|
84cbcc9559 | ||
|
|
561021e899 | ||
|
|
55c57292d9 | ||
|
|
522e88e89d | ||
|
|
313f21fbd7 | ||
|
|
3ba46fd780 | ||
|
|
6f6b844455 | ||
|
|
3d246efded | ||
|
|
a2d9249bd1 | ||
|
|
0f915ba4c5 | ||
|
|
bad1202fe6 | ||
|
|
38999ccf77 | ||
|
|
1a6d5c6b0d | ||
|
|
1a98728cce | ||
|
|
90c469b699 | ||
|
|
65f6f882b0 | ||
|
|
6038d9696a | ||
|
|
e848fd6f95 | ||
|
|
3b909f6f60 | ||
|
|
a726a24549 | ||
|
|
291be9edb6 | ||
|
|
7d172cf57b | ||
|
|
6f11dadf30 | ||
|
|
bbf4d0dfb2 | ||
|
|
5c2458cc9f | ||
|
|
5287ba7007 | ||
|
|
6627c5c07f | ||
|
|
136d4fb99b | ||
|
|
fadd63ffbc | ||
|
|
0430f7d403 | ||
|
|
aef8d13335 | ||
|
|
aeac386099 | ||
|
|
c4863498ec | ||
|
|
72627d1435 | ||
|
|
18c8f0ce87 | ||
|
|
e839c7dc00 | ||
|
|
adede4bc99 | ||
|
|
2646a3216b | ||
|
|
6020acfd36 | ||
|
|
42020bd637 | ||
|
|
88d85ad2f5 | ||
|
|
723d7abce7 | ||
|
|
ff7e36c043 | ||
|
|
94201ae9f6 | ||
|
|
b2d2158b61 | ||
|
|
370815562a | ||
|
|
431cd82e33 | ||
|
|
f66a9ff3a3 | ||
|
|
bfee35d523 | ||
|
|
377f7bbe0a | ||
|
|
03785ea481 | ||
|
|
08f1c06940 | ||
|
|
0ae1eccaf9 | ||
|
|
a1404bb21d | ||
|
|
34be127621 | ||
|
|
a4488ce79f | ||
|
|
178f5edd88 | ||
|
|
772b61655e | ||
|
|
b7f795fa9e | ||
|
|
1a39152144 | ||
|
|
071a39e576 | ||
|
|
1f192d528d | ||
|
|
b107f70fd7 | ||
|
|
77c8258831 | ||
|
|
e762cdd554 | ||
|
|
20c9f4c7ac | ||
|
|
7d2b370aec | ||
|
|
fea4e861a1 | ||
|
|
ec58be55ec | ||
|
|
d10d332ab3 | ||
|
|
0d72bd811f | ||
|
|
9feeadd70c | ||
|
|
fd94fb5898 | ||
|
|
02c17dcf69 | ||
|
|
b670047b03 | ||
|
|
6132352be1 | ||
|
|
5afa9456f9 | ||
|
|
4bea90dc0f | ||
|
|
610f683e4a | ||
|
|
502e121bd2 | ||
|
|
4afce35695 | ||
|
|
254641d184 | ||
|
|
647d5da395 | ||
|
|
2ef0405d69 | ||
|
|
55b7158f9d | ||
|
|
361dea9a18 | ||
|
|
7a266053dc | ||
|
|
fe5ceff991 | ||
|
|
e420d47298 | ||
|
|
af8119749b | ||
|
|
5d3f1a2940 | ||
|
|
5d1e010df5 | ||
|
|
4ce9aaeb5a | ||
|
|
3875f34988 | ||
|
|
88b7c40567 | ||
|
|
d217d53ee0 | ||
|
|
2513368cdd | ||
|
|
a52c3ca659 | ||
|
|
3289884136 | ||
|
|
4a5dc408df | ||
|
|
0b1add6cb3 |
No files matched your search
@@ -0,0 +1 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -0,0 +1,208 @@
|
||||
name: Build and Version Update
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- merge
|
||||
- dev
|
||||
- fabric-experimental-gradle
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# 2. Install JDK 21 – NO Gradle cache here
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: '' # ← disable Maven/Gradle cache
|
||||
|
||||
# 3. Set up Gradle + dependency cache
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
|
||||
- name: Generate build number
|
||||
id: buildnumber
|
||||
uses: einaregilsson/build-number@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update version in build.gradle.kts
|
||||
id: update_version
|
||||
run: |
|
||||
# Read current fullVersion from build.gradle.kts
|
||||
FULL_VERSION=$(grep 'val fullVersion =' build.gradle.kts | awk -F '"' '{print $2}')
|
||||
IFS='.' read -ra VERSION_PARTS <<< "$FULL_VERSION"
|
||||
PATCH=$((VERSION_PARTS[2] + 1))
|
||||
NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.$PATCH-dev-b${{ steps.buildnumber.outputs.build_number }}"
|
||||
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
# Update fullVersion in build.gradle
|
||||
sed -i "s/val fullVersion = \".*\"/val fullVersion = \"$NEW_VERSION\"/" build.gradle.kts
|
||||
|
||||
# Ensure snapshot is set to false
|
||||
sed -i "s/val snapshot = .*/val snapshot = false/" build.gradle.kts
|
||||
|
||||
- name: Build without PE shaded
|
||||
run: ./gradlew build -PshadePE=false -x test
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
|
||||
- name: Build with PE shaded Gradle
|
||||
run: ./gradlew build -PshadePE=true -x test
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
|
||||
# Get the Hashes of the Original and Lite JARs
|
||||
- name: Calculate Hashes
|
||||
run: |
|
||||
# Calculate hash for the original JAR
|
||||
BUKKIT_HASH=$(sha256sum bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar | awk '{print $1}')
|
||||
echo "BUKKIT_HASH=$BUKKIT_HASH" >> $GITHUB_ENV
|
||||
# Calculate hash for the lite JAR
|
||||
BUKKIT_LITE_HASH=$(sha256sum bukkit/build/libs/knockbacksync-bukkit-lite-${{ env.NEW_VERSION }}.jar | awk '{print $1}')
|
||||
echo "BUKKIT_LITE_HASH=$BUKKIT_LITE_HASH" >> $GITHUB_ENV
|
||||
# Calculate hash for the lite JAR
|
||||
FABRIC_HASH=$(sha256sum fabric/build/libs/knockbacksync-fabric-${{ env.NEW_VERSION }}.jar | awk '{print $1}')
|
||||
echo "FABRIC_HASH=$FABRIC_HASH" >> $GITHUB_ENV
|
||||
|
||||
# Download, update, and re-upload dev-builds.txt
|
||||
- name: Update dev-builds.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Download the current dev-builds.txt
|
||||
curl -L -o dev-builds.txt https://github.com/${{ github.repository }}/releases/latest/download/dev-builds.txt
|
||||
|
||||
# Create a copy of the original file
|
||||
cp dev-builds.txt dev-builds.txt.original
|
||||
|
||||
# Ensure the file ends with a newline
|
||||
sed -i -e '$a\' dev-builds.txt
|
||||
|
||||
# Function to add hash if it doesn't exist
|
||||
add_hash_if_new() {
|
||||
if ! grep -q "$1" dev-builds.txt; then
|
||||
echo "$1" >> dev-builds.txt
|
||||
fi
|
||||
}
|
||||
|
||||
# Add new hashes if they don't exist
|
||||
add_hash_if_new "${{ env.BUKKIT_HASH }}"
|
||||
add_hash_if_new "${{ env.BUKKIT_LITE_HASH }}"
|
||||
add_hash_if_new "${{ env.FABRIC_HASH }}"
|
||||
|
||||
# Check if the file has changed
|
||||
if cmp -s dev-builds.txt dev-builds.txt.original; then
|
||||
echo "No changes to dev-builds.txt, skipping upload."
|
||||
else
|
||||
echo "Changes detected in dev-builds.txt, uploading new version."
|
||||
|
||||
# Get the latest release ID
|
||||
RELEASE_ID=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r .id)
|
||||
|
||||
# Delete the old dev-builds.txt asset
|
||||
ASSET_ID=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets" | \
|
||||
jq -r '.[] | select(.name == "dev-builds.txt") | .id')
|
||||
if [ ! -z "$ASSET_ID" ]; then
|
||||
curl -X DELETE -H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$ASSET_ID"
|
||||
fi
|
||||
|
||||
# Upload the new dev-builds.txt
|
||||
curl -X POST -H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @dev-builds.txt \
|
||||
"https://uploads.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets?name=dev-builds.txt"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm dev-builds.txt.original
|
||||
|
||||
- name: Upload Bukkit Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar
|
||||
path: bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload Bukkit Lite Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: knockbacksync-bukkit-lite-${{ env.NEW_VERSION }}.jar
|
||||
path: bukkit/build/libs/knockbacksync-bukkit-lite-${{ env.NEW_VERSION }}.jar
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload Fabric Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: knockbacksync-fabric-${{ env.NEW_VERSION }}.jar
|
||||
path: fabric/build/libs/knockbacksync-fabric-${{ env.NEW_VERSION }}.jar
|
||||
retention-days: 90
|
||||
|
||||
- uses: Kir-Antipov/mc-publish@v3.3
|
||||
with:
|
||||
modrinth-id: ${{ vars.MODRINTH_ID }}
|
||||
modrinth-featured: true
|
||||
modrinth-unfeature-mode: subset
|
||||
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
|
||||
|
||||
files: |
|
||||
bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar
|
||||
bukkit/build/libs/knockbacksync-bukkit-lite-${{ env.NEW_VERSION }}.jar
|
||||
|
||||
name: KnockbackSync-${{ env.NEW_VERSION }}
|
||||
version: ${{ env.NEW_VERSION }}
|
||||
version-type: alpha
|
||||
|
||||
loaders: |
|
||||
bukkit
|
||||
spigot
|
||||
paper
|
||||
folia
|
||||
purpur
|
||||
|
||||
game-versions: |
|
||||
>=1.12.2
|
||||
|
||||
retry-attempts: 2
|
||||
retry-delay: 10000
|
||||
fail-mode: fail
|
||||
|
||||
- uses: Kir-Antipov/mc-publish@v3.3
|
||||
with:
|
||||
modrinth-id: ${{ vars.MODRINTH_ID }}
|
||||
modrinth-featured: true
|
||||
modrinth-unfeature-mode: subset
|
||||
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
|
||||
|
||||
files: |
|
||||
fabric/build/libs/knockbacksync-fabric-${{ env.NEW_VERSION }}.jar
|
||||
|
||||
name: KnockbackSync-${{ env.NEW_VERSION }}
|
||||
version: ${{ env.NEW_VERSION }}
|
||||
version-type: alpha
|
||||
|
||||
loaders: |
|
||||
fabric
|
||||
|
||||
game-versions: |
|
||||
1.21.11
|
||||
|
||||
dependencies: |
|
||||
fabric-api
|
||||
|
||||
retry-attempts: 2
|
||||
retry-delay: 10000
|
||||
fail-mode: fail
|
||||
+10
@@ -6,3 +6,13 @@ dependency-reduced-pom.xml
|
||||
|
||||
# Ignore maven build files
|
||||
target/
|
||||
|
||||
# Eclipse files
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
run/
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,35 @@
|
||||
## Project Structure
|
||||
|
||||
- `common` submodule contains shared code for all platforms.
|
||||
- It contains the interfaces and abstractions for
|
||||
- Permissions
|
||||
- Scheduling
|
||||
- Players
|
||||
- Blocks
|
||||
- Worlds/Levels
|
||||
- The Server
|
||||
- In addition to the interfaces, the code also contains the implementations for
|
||||
- Commands
|
||||
- Commands use incendio's `cloud` command system. All you have to do to add platform support is to create a CommandManager and call register().
|
||||
- Handling Events
|
||||
- Gathering Statistics
|
||||
- `fabric` contains code for latest fabric version (currently 1.21)
|
||||
- `bukkit` contains bukkit-specific code
|
||||
|
||||
If you want to add support for a new platform, simply make a new submodule and implement the interfaces for.
|
||||
- Scheduling Tasks
|
||||
- Checking for Permissions
|
||||
- Listening for events and calling the right handler
|
||||
- Players + Blocks + World + Server
|
||||
- Metrics
|
||||
|
||||
You will need to handle events when Players:
|
||||
- join/leave
|
||||
- are damaged
|
||||
- take velocity (knock back)
|
||||
|
||||
If you find your platform is lacking events, take a look at how the PlayerVelocityEvent is implemented fabric for some direction of what to do
|
||||
|
||||
|
||||
## List of version and platform-specific caveats
|
||||
- Runnable and manual ping does not work on fabric right now due b/c packetevents. Ping measurement will be less accurate
|
||||
@@ -5,6 +5,7 @@
|
||||
<configuration>
|
||||
<autoDetectTypes>
|
||||
<platformType>SPIGOT</platformType>
|
||||
<platformType>ADVENTURE</platformType>
|
||||
</autoDetectTypes>
|
||||
<projectReimportVersion>1</projectReimportVersion>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
KnockbackSync
|
||||
=============
|
||||
|
||||
KnockbackSync is licensed under the GNU General Public License v3.0 or later.
|
||||
See README.md (section "License") and https://www.gnu.org/licenses/gpl-3.0.en.html.
|
||||
|
||||
Bundled / ported third-party code
|
||||
----------------------------------
|
||||
|
||||
This distribution contains a port of the 1.8 melee knockback formula from:
|
||||
|
||||
BukkitOldCombatMechanics (OldCombatMechanics)
|
||||
Copyright (c) the OldCombatMechanics contributors (kernitus et al.)
|
||||
https://github.com/kernitus/BukkitOldCombatMechanics
|
||||
Licensed under the Mozilla Public License, Version 2.0 (MPL-2.0)
|
||||
https://mozilla.org/MPL/2.0/
|
||||
|
||||
The ported code lives in:
|
||||
|
||||
common/src/main/java/me/caseload/knockbacksync/util/LegacyKnockback.java
|
||||
|
||||
It is derived from OldCombatMechanics' module
|
||||
`kernitus.plugin.OldCombatMechanics.module.ModulePlayerKnockback`.
|
||||
|
||||
License compatibility
|
||||
---------------------
|
||||
|
||||
The OldCombatMechanics source files are distributed under the standard MPL-2.0
|
||||
notice (Exhibit A) and do NOT carry the "Incompatible With Secondary Licenses"
|
||||
notice (Exhibit B). Under MPL-2.0 section 3.3, such Covered Software may be
|
||||
distributed as part of a Larger Work governed by a "Secondary License" — which
|
||||
MPL-2.0 section 1.12 defines to include the GNU General Public License, Version
|
||||
3. Accordingly, the ported file may be combined with and distributed alongside
|
||||
the GPL-3.0-or-later code that makes up the rest of KnockbackSync.
|
||||
|
||||
The terms of the MPL-2.0 continue to govern the ported file itself; its MPL
|
||||
header is retained. The combined/Larger Work is offered under GPL-3.0-or-later.
|
||||
@@ -1,29 +1,15 @@
|
||||
Minecraft doesn’t factor in network latency when determining a player's actions on the server.
|
||||
This causes the server to receive outdated information that doesn’t reflect the player's clientside position.
|
||||
As a result, players take negative velocity when they're on the ground clientside, but not serverside.
|
||||
# KnockbackSync-OG
|
||||
|
||||
This plugin handles knockback as if it were calculated clientside, ensuring that no player is at a disadvantage,
|
||||
regardless of their own or their opponent’s connection.
|
||||
|
||||
Showcase: https://www.youtube.com/watch?v=SVokpr3v-TA
|
||||
|
||||
Official Discord: https://discord.gg/nnpqpAtyVW
|
||||
|
||||
## Frequently Asked Questions (FAQ)
|
||||
|
||||
### Does this change put high ping players at a disadvantage?
|
||||
**It depends on the player.** Some may notice a difference if they're used to relying on high ping to reduce knockback. For others, it could actually be an advantage.
|
||||
|
||||
### How does this change benefit high ping players?
|
||||
**Knockback control.** For example, it will be easier to escape crit chains and punish crit.
|
||||
|
||||
### Why was the configurability of ping offset removed?
|
||||
**It promotes consistency across all servers.** Extensive testing with top players has shown that an offset of 25 provides a balanced experience for everyone.
|
||||
|
||||
### How do I change the ping offset?
|
||||
**You must run a modified build of KnockbackSync.** The variable can be changed inside of the [PlayerData](src/main/java/me/caseload/knockbacksync/manager/PlayerData.java) class.
|
||||
Soft fork of KnockbackSync to ensure compatibility with OldCombatMechanics on Purpur 1.19.4.
|
||||
|
||||
## License
|
||||
GNU General Public License v3.0 or later
|
||||
|
||||
See [COPYING](COPYING) to see the full text.
|
||||
See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.en.html) to see the full text.
|
||||
|
||||
The optional legacy (1.8) knockback formula
|
||||
(`common/src/main/java/me/caseload/knockbacksync/util/LegacyKnockback.java`) is a
|
||||
port from [OldCombatMechanics](https://github.com/kernitus/BukkitOldCombatMechanics),
|
||||
which is licensed under the Mozilla Public License 2.0 (MPL-2.0). MPL-2.0 permits
|
||||
combining that code into this GPL-3.0-or-later work as a Larger Work (MPL-2.0
|
||||
§3.3 / §1.12). See the [NOTICE](NOTICE) file for details.
|
||||
@@ -0,0 +1,105 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
eclipse
|
||||
id("com.gradleup.shadow") version "9.2.2" apply false
|
||||
}
|
||||
|
||||
fun getGitCommitHash(project: Project): String? {
|
||||
// Only try to get the hash if the .git directory exists
|
||||
if (!project.file(".git").isDirectory) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
|
||||
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
.redirectError(ProcessBuilder.Redirect.PIPE)
|
||||
.start()
|
||||
|
||||
process.waitFor(5, TimeUnit.SECONDS)
|
||||
|
||||
if (process.exitValue() == 0) {
|
||||
process.inputStream.bufferedReader().readText().trim().takeIf { it.isNotEmpty() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val fullVersion = "1.3.5"
|
||||
val snapshot = true
|
||||
val githubRepo = System.getenv("GITHUB_REPOSITORY") ?: project.findProperty("githubRepo").toString()
|
||||
|
||||
// Suite default external provider; shaded standalone via -PshadePE=true.
|
||||
extra["shadePE"] = project.findProperty("shadePE")?.toString()?.toBoolean()
|
||||
?: System.getenv("SHADE_PE")?.toBoolean()
|
||||
?: false
|
||||
|
||||
extra["relocate"] = project.findProperty("relocate")?.toString()?.toBoolean()
|
||||
?: System.getenv("RELOCATE_JAR")?.toBoolean()
|
||||
?: true
|
||||
|
||||
allprojects {
|
||||
fun getVersionMeta(includeHash: Boolean): String {
|
||||
if (!snapshot) {
|
||||
return ""
|
||||
}
|
||||
val commitHash = if (includeHash) {
|
||||
getGitCommitHash(project)?.let { "+$it" } ?: ""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
return "$commitHash-SNAPSHOT"
|
||||
}
|
||||
|
||||
group = "me.caseload.knockbacksync"
|
||||
version = "$fullVersion${getVersionMeta(true)}"
|
||||
ext["versionNoHash"] = "$fullVersion${getVersionMeta(false)}"
|
||||
ext["githubRepo"] = githubRepo
|
||||
|
||||
repositories {
|
||||
// mavenLocal()
|
||||
maven("https://repo.grim.ac/snapshots")
|
||||
mavenCentral()
|
||||
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
|
||||
maven("https://repo.codemc.io/repository/maven-releases/")
|
||||
maven("https://repo.opencollab.dev/maven-snapshots/")
|
||||
maven("https://repo.papermc.io/repository/maven-public/")
|
||||
maven(url = "https://maven.fabricmc.net/") {
|
||||
name = "Fabric"
|
||||
}
|
||||
maven("https://libraries.minecraft.net/")
|
||||
maven("https://maven.neoforged.net/releases")
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
// The root project has no plugin entrypoint; do not emit an empty deployable-looking JAR.
|
||||
enabled = false
|
||||
}
|
||||
|
||||
val copyBukkitPluginJar by tasks.registering(Copy::class) {
|
||||
val bukkitShadowJar = project(":bukkit").tasks.named<ShadowJar>("shadowJar")
|
||||
|
||||
dependsOn(bukkitShadowJar)
|
||||
from(bukkitShadowJar.flatMap { it.archiveFile })
|
||||
into(layout.buildDirectory.dir("libs"))
|
||||
rename { "KnockbackSync-OG-$fullVersion${if (snapshot) "-SNAPSHOT" else ""}.jar" }
|
||||
}
|
||||
|
||||
tasks.assemble {
|
||||
dependsOn(copyBukkitPluginJar)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("com.gradleup.shadow")
|
||||
}
|
||||
|
||||
val shadePE: Boolean by rootProject.extra
|
||||
|
||||
base {
|
||||
archivesName.set("${rootProject.property("archives_base_name")}-bukkit${if (shadePE) "" else "-lite"}")
|
||||
}
|
||||
|
||||
val shadeThisThing: Configuration by configurations.creating {
|
||||
isCanBeConsumed = true
|
||||
isTransitive = true
|
||||
}
|
||||
|
||||
// TODO migrate to only including sources sets for compile, test and javadoc tasks
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
source(project(":common").sourceSets.main.get().allSource)
|
||||
}
|
||||
|
||||
tasks.withType<Javadoc>().configureEach {
|
||||
source(project(":common").sourceSets.main.get().allJava)
|
||||
}
|
||||
|
||||
tasks.named<JavaCompile>("compileTestJava") {
|
||||
exclude("**/*")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT")
|
||||
compileOnly("org.geysermc.floodgate:api:2.0-SNAPSHOT")
|
||||
compileOnly("io.netty:netty-all:4.1.72.Final")
|
||||
|
||||
compileOnly("org.projectlombok:lombok:1.18.34")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.34")
|
||||
|
||||
shadeThisThing(implementation("org.kohsuke:github-api:1.326")!!)
|
||||
if (shadePE) {
|
||||
shadeThisThing(implementation("com.github.retrooper:packetevents-spigot:2.11.2")!!)
|
||||
} else {
|
||||
compileOnly("com.github.retrooper:packetevents-spigot:2.11.2")
|
||||
}
|
||||
shadeThisThing(implementation("org.incendo:cloud-paper:2.0.0-beta.14")!!)
|
||||
shadeThisThing(implementation("org.incendo:cloud-core:2.0.0")!!)
|
||||
|
||||
// Required for 1.14.4 support because gson is too old to have JosnParser.parseString()
|
||||
shadeThisThing(implementation("com.google.code.gson:gson:2.11.0")!!)
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
manifest {
|
||||
attributes["paperweight-mappings-namespace"] = "mojang"
|
||||
}
|
||||
|
||||
// Remove the -all suffix from the output JAR file name
|
||||
archiveClassifier.set("")
|
||||
|
||||
configurations = listOf(shadeThisThing)
|
||||
enableAutoRelocation = true
|
||||
relocationPrefix = "${project.property("maven_group")}.${project.property("archives_base_name")}.shaded"
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
// The shaded archive is the runtime plugin; do not emit a misleading thin plugin JAR.
|
||||
enabled = false
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(tasks.shadowJar)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
from(project(":common").sourceSets.main.get().resources)
|
||||
inputs.property("version", project.version)
|
||||
inputs.property("shadePE", shadePE) // Add shadePE as an input to trigger reprocessing if it changes
|
||||
filteringCharset = "UTF-8"
|
||||
|
||||
|
||||
filesMatching("plugin.yml") {
|
||||
expand(
|
||||
"version" to project.version,
|
||||
// Bukkit accepts `depend` only; `depends` is silently ignored.
|
||||
"depend" to if (shadePE) "[]" else listOf("PacketEvents")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package me.caseload.knockbacksync;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.protocol.world.Location;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import me.caseload.knockbacksync.common.BuildConfig;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.listener.bukkit.*;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.permission.PermissionChecker;
|
||||
import me.caseload.knockbacksync.permission.PluginPermissionChecker;
|
||||
import me.caseload.knockbacksync.scheduler.BukkitSchedulerAdapter;
|
||||
import me.caseload.knockbacksync.scheduler.FoliaSchedulerAdapter;
|
||||
import me.caseload.knockbacksync.sender.BukkitPlayerSelectorParser;
|
||||
import me.caseload.knockbacksync.sender.BukkitSenderFactory;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.stats.custom.BukkitStatsManager;
|
||||
import me.caseload.knockbacksync.stats.custom.PluginJarHashProvider;
|
||||
import me.caseload.knockbacksync.world.BukkitServer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
|
||||
import org.incendo.cloud.execution.ExecutionCoordinator;
|
||||
import org.incendo.cloud.paper.LegacyPaperCommandManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class BukkitBase extends Base {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final BukkitSenderFactory bukkitSenderFactory = new BukkitSenderFactory(this);
|
||||
private final PluginPermissionChecker permissionChecker = new PluginPermissionChecker();
|
||||
|
||||
private int playerUpdateInterval;
|
||||
|
||||
private final MethodHandle tickRateMethodHandle;
|
||||
|
||||
public BukkitBase(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
super.configManager = new ConfigManager();
|
||||
super.playerSelectorParser = new BukkitPlayerSelectorParser<>();
|
||||
super.statsManager = new BukkitStatsManager(plugin);
|
||||
super.platformServer = new BukkitServer();
|
||||
super.pluginJarHashProvider = new PluginJarHashProvider(this.getClass().getProtectionDomain().getCodeSource().getLocation());
|
||||
this.playerUpdateInterval = this.getConfigManager().getConfigWrapper().getInt("entity_tick_intervals.player", 2);
|
||||
|
||||
MethodHandle handle = null;
|
||||
try {
|
||||
Server craftServer = Bukkit.getServer();
|
||||
MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
|
||||
// Get handle for getServerTickManager
|
||||
MethodHandle getServerTickManager = lookup.findVirtual(craftServer.getClass(),
|
||||
"getServerTickManager", MethodType.methodType(Object.class));
|
||||
|
||||
Object serverTickManager = getServerTickManager.invoke(craftServer);
|
||||
|
||||
// Get handle for getTickRate
|
||||
handle = lookup.findVirtual(serverTickManager.getClass(),
|
||||
"getTickRate", MethodType.methodType(float.class));
|
||||
|
||||
} catch (Throwable t) {
|
||||
// If anything fails, handle will remain null
|
||||
}
|
||||
this.tickRateMethodHandle = handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Logger getLogger() {
|
||||
return this.plugin.getLogger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDataFolder() {
|
||||
return this.plugin.getDataFolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getResource(String filename) {
|
||||
return this.plugin.getResource(filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
// External-provider mode delegates PacketEvents lifecycle to the installed PE plugin.
|
||||
if (!BuildConfig.SHADE_PE) {
|
||||
return;
|
||||
}
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this.plugin));
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
super.enable();
|
||||
super.eventBus.registerListeners(this);
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_5) && this.getPlatform() == Platform.BUKKIT) {
|
||||
scheduler.runTaskTimerAsynchronously(this::setUpdateIntervals, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeScheduler() {
|
||||
switch (getPlatform()) {
|
||||
case BUKKIT:
|
||||
super.scheduler = new BukkitSchedulerAdapter(this.plugin);
|
||||
break;
|
||||
case FOLIA:
|
||||
super.scheduler = new FoliaSchedulerAdapter(this.plugin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerPlatformListeners() {
|
||||
registerPluginListeners(
|
||||
new BukkitPlayerDamageListener(),
|
||||
new BukkitPlayerKnockbackListener(),
|
||||
new me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier()
|
||||
);
|
||||
// Reconcile away any leftover owned knockback-resistance marker for players already
|
||||
// online (e.g. across a /reload), so none is stranded.
|
||||
me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier.cleanupAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerCommands() {
|
||||
super.commandManager = new LegacyPaperCommandManager<>(
|
||||
this.plugin,
|
||||
ExecutionCoordinator.simpleCoordinator(),
|
||||
bukkitSenderFactory
|
||||
);
|
||||
if (commandManager instanceof LegacyPaperCommandManager) {
|
||||
LegacyPaperCommandManager<Sender> legacyPaperCommandManager = (LegacyPaperCommandManager<Sender>) commandManager;
|
||||
if (legacyPaperCommandManager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
|
||||
legacyPaperCommandManager.registerBrigadier();
|
||||
} else if (commandManager.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
|
||||
legacyPaperCommandManager.registerAsynchronousCompletions();
|
||||
}
|
||||
}
|
||||
super.registerCommands();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getVersion() {
|
||||
return this.plugin.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDefaultConfig() {
|
||||
this.plugin.saveDefaultConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionChecker getPermissionChecker() {
|
||||
return permissionChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTickRate() {
|
||||
if (tickRateMethodHandle != null) {
|
||||
try {
|
||||
return (float) tickRateMethodHandle.invoke();
|
||||
} catch (Throwable t) {
|
||||
return 20.0f;
|
||||
}
|
||||
}
|
||||
return 20.0f;
|
||||
}
|
||||
|
||||
public URL getJarURL() {
|
||||
try {
|
||||
String path = getClass().getProtectionDomain().getCodeSource().getLocation().getPath();
|
||||
File pluginJarFile = new File(path);
|
||||
|
||||
return Paths.get(getDataFolder().getParentFile().getAbsolutePath(), pluginJarFile.getName()).toUri().toURL();
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Couldn't find plugin file: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPluginListeners(Listener... listeners) {
|
||||
PluginManager pluginManager = this.plugin.getServer().getPluginManager();
|
||||
for (Listener listener : listeners)
|
||||
pluginManager.registerEvents(listener, this.plugin);
|
||||
}
|
||||
|
||||
public BukkitSenderFactory getSenderFactory() {
|
||||
return this.bukkitSenderFactory;
|
||||
}
|
||||
|
||||
public void setUpdateIntervals() {
|
||||
try {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
Method getWorldHandleMethod = world.getClass().getMethod("getHandle");
|
||||
Object serverLevel = getWorldHandleMethod.invoke(world);
|
||||
|
||||
// Get ChunkMap
|
||||
Method getChunkSource = serverLevel.getClass().getMethod("getChunkSource");
|
||||
Object chunkSource = getChunkSource.invoke(serverLevel);
|
||||
Field chunkMapField = chunkSource.getClass().getDeclaredField("chunkMap");
|
||||
chunkMapField.setAccessible(true);
|
||||
Object chunkMap = chunkMapField.get(chunkSource);
|
||||
|
||||
// Get entityMap from ChunkMap
|
||||
Field entityMapField = chunkMap.getClass().getDeclaredField("entityMap");
|
||||
entityMapField.setAccessible(true);
|
||||
Map<Integer, ?> entityMap = (Map<Integer, ?>) entityMapField.get(chunkMap);
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
Object trackedEntity = entityMap.get(player.getEntityId());
|
||||
if (trackedEntity == null)
|
||||
continue;
|
||||
|
||||
Field serverEntityField = trackedEntity.getClass().getDeclaredField("serverEntity");
|
||||
serverEntityField.setAccessible(true);
|
||||
Object serverEntity = serverEntityField.get(trackedEntity);
|
||||
|
||||
Field updateIntervalField = serverEntity.getClass().getDeclaredField("updateInterval");
|
||||
updateIntervalField.setAccessible(true);
|
||||
updateIntervalField.set(serverEntity, playerUpdateInterval);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) {
|
||||
throw new IllegalStateException("Unable to use reflection to modify updateIntervals" + e);
|
||||
}
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReload(ConfigReloadEvent event) {
|
||||
playerUpdateInterval = event.getConfigManager().getConfigWrapper().getInt("entity_tick_intervals.player", 2);
|
||||
}
|
||||
|
||||
public void restartServer() {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
|
||||
}
|
||||
|
||||
public boolean hasRestartScript() {
|
||||
File spigotFile = new File("spigot.yml");
|
||||
if (!spigotFile.exists())
|
||||
return false;
|
||||
|
||||
FileConfiguration spigotConfig = YamlConfiguration.loadConfiguration(spigotFile);
|
||||
String scriptValue = spigotConfig.getString("settings.restart-script");
|
||||
if (scriptValue == null || scriptValue.isEmpty())
|
||||
return false;
|
||||
|
||||
return Paths.get(scriptValue).toAbsolutePath().normalize().toFile().exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package me.caseload.knockbacksync;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import me.caseload.knockbacksync.common.BuildConfig;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class BukkitLoaderPlugin extends JavaPlugin {
|
||||
|
||||
private final Base core = new BukkitBase(this);
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
core.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
core.enable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Reconcile away any leftover owned knockback-resistance marker so no player is left
|
||||
// with reduced resistance after the plugin unloads.
|
||||
me.caseload.knockbacksync.listener.bukkit.KnockbackResistanceNullifier.cleanupAll();
|
||||
// Drop listener handles before any lifecycle decision; the shared API may outlive this plugin.
|
||||
core.unregisterPacketListeners();
|
||||
// Terminate only when this plugin owns the PacketEvents lifecycle (shaded build).
|
||||
if (BuildConfig.SHADE_PE) {
|
||||
PacketEvents.getAPI().terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package me.caseload.knockbacksync.api;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* Stable, reflection-friendly entry point for other plugins (notably OldCombatMechanics'
|
||||
* old-fishing-knockback module) to hand KnockbackSync a pre-computed knockback vector, so that
|
||||
* KnockbackSync owns the final velocity and applies the same latency Y-correction it gives melee
|
||||
* knockback.
|
||||
*
|
||||
* <p>Why this exists: a fishing-rod hit reaches KnockbackSync as an ordinary player-vs-player
|
||||
* damage event. KnockbackSync cannot tell it apart from a sword hit, so it computes a melee vector
|
||||
* and the caller's own {@code setVelocity} either fights it or is re-corrected with melee-tuned
|
||||
* latency logic — the rod pop ends up flattened/inconsistent under latency. Routing the
|
||||
* bobber-direction vector through here makes the rod feel like 1.8, latency-synced like melee.</p>
|
||||
*
|
||||
* <p>Resolved by callers via reflection; keep the class name, package, and method signature
|
||||
* stable.</p>
|
||||
*/
|
||||
public final class KnockbackSyncRodApi {
|
||||
|
||||
private KnockbackSyncRodApi() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a fishing-rod knockback vector for {@code victim}, to be applied and latency-synced
|
||||
* by the {@code PlayerVelocityEvent} produced by the caller's subsequent damage tick.
|
||||
*
|
||||
* <p>MUST be called on the main thread, immediately before the caller deals the rod's damage.</p>
|
||||
*
|
||||
* @param victim the player being knocked back
|
||||
* @param rodKnockback the 1.8 bobber-direction knockback vector
|
||||
* @return {@code true} if KnockbackSync will own this knockback (caller must NOT also call
|
||||
* {@code setVelocity}); {@code false} if KnockbackSync is disabled or not tracking this
|
||||
* player, in which case the caller should apply the knockback itself.
|
||||
*/
|
||||
public static boolean applyRodKnockback(Player victim, Vector rodKnockback) {
|
||||
if (victim == null || rodKnockback == null) return false;
|
||||
|
||||
final Base base = Base.INSTANCE;
|
||||
if (base == null) return false;
|
||||
|
||||
final ConfigManager config = base.getConfigManager();
|
||||
if (config == null || !config.isToggled() || !config.isLegacyKnockbackEnabled())
|
||||
return false;
|
||||
|
||||
final User user = new BukkitPlayer(victim).getUser();
|
||||
if (user == null) return false;
|
||||
|
||||
final PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData == null) return false; // not tracked (exempt/Geyser/etc.) — caller falls back
|
||||
|
||||
final Vector3d kb = new Vector3d(rodKnockback.getX(), rodKnockback.getY(), rodKnockback.getZ());
|
||||
playerData.setRodKnockback(kb);
|
||||
// On-ground sync should target the rod's vertical, mirroring the melee damage path.
|
||||
playerData.setVerticalVelocity(kb.getY());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package me.caseload.knockbacksync.listener.bukkit;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.listener.PlayerDamageListener;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.util.MultiLibUtil;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
|
||||
public class BukkitPlayerDamageListener extends PlayerDamageListener implements Listener {
|
||||
|
||||
/**
|
||||
* D3 — projectile PvP knockback-resistance nullification.
|
||||
*
|
||||
* <p>For a hit that {@link PvpSource} classifies as {@code PROJECTILE} PvP, zero the
|
||||
* victim's effective knockback resistance BEFORE vanilla computes the knockback, so a
|
||||
* netherite victim takes full (diamond-level) projectile knockback. Runs at {@code LOW}
|
||||
* so it precedes OCM's {@code projectile-knockback} damage raise ({@code NORMAL}) and
|
||||
* vanilla knockback. The decision is gated on classification + config only — never on the
|
||||
* current event damage, because OCM may not have raised snowball/egg/ender-pearl damage
|
||||
* yet at this priority.</p>
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onProjectilePvpDamage(EntityDamageByEntityEvent event) {
|
||||
final ConfigManager config = Base.INSTANCE.getConfigManager();
|
||||
// Single source of truth: active only when toggled on, legacy knockback enabled,
|
||||
// and knockback resistance is NOT being kept (matching OCM's default).
|
||||
if (!config.isToggled() || !config.isLegacyKnockbackEnabled() || config.isLegacyKnockbackResistanceEnabled())
|
||||
return;
|
||||
|
||||
if (PvpSource.classify(event) != PvpSource.Type.PROJECTILE)
|
||||
return;
|
||||
|
||||
final Player victim = (Player) event.getEntity();
|
||||
if (MultiLibUtil.isExternalPlayer(victim))
|
||||
return;
|
||||
|
||||
KnockbackResistanceNullifier.nullify(victim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Melee PvP: KnockbackSync owns the 1.8 melee velocity via {@code LegacyKnockback}
|
||||
* (applied at {@code PlayerVelocityEvent}), so melee already delivers full knockback
|
||||
* without touching the resistance attribute. The previous {@code stripKnockbackResistance()}
|
||||
* (which removed the victim's {@code KNOCKBACK_RESISTANCE} modifiers and never restored them,
|
||||
* leaking into PvE) has been deleted. Projectile/explosion/fishing hits are NOT routed
|
||||
* through this melee path (their damager is not a player), so no stale legacy-knockback
|
||||
* vector can be stored for them.
|
||||
*/
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerDamage(EntityDamageByEntityEvent event) {
|
||||
Entity victim = event.getEntity();
|
||||
Entity attacker = event.getDamager();
|
||||
if (!(victim instanceof Player) || !(attacker instanceof Player))
|
||||
return;
|
||||
|
||||
if (MultiLibUtil.isExternalPlayer((Player) victim))
|
||||
return;
|
||||
|
||||
onPlayerDamage(new BukkitPlayer((Player) victim), new BukkitPlayer((Player) attacker));
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package me.caseload.knockbacksync.listener.bukkit;
|
||||
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.listener.PlayerKnockbackListener;
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.util.MultiLibUtil;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.player.PlayerVelocityEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public class BukkitPlayerKnockbackListener extends PlayerKnockbackListener implements Listener {
|
||||
|
||||
// HIGHEST so KnockbackSync is the last writer in the event chain: it owns the final
|
||||
// melee velocity. We read and write the EVENT velocity (not player.getVelocity()/
|
||||
// player.setVelocity()) so we integrate with the chain instead of fighting it. See
|
||||
// knockback-sync issue #7 (kernitus, Krymonota, Axionize).
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerVelocity(PlayerVelocityEvent event) {
|
||||
Player victim = event.getPlayer();
|
||||
EntityDamageEvent entityDamageEvent = victim.getLastDamageCause();
|
||||
if (entityDamageEvent == null)
|
||||
return;
|
||||
|
||||
EntityDamageEvent.DamageCause damageCause = entityDamageEvent.getCause();
|
||||
if (damageCause != EntityDamageEvent.DamageCause.ENTITY_ATTACK)
|
||||
return;
|
||||
|
||||
Entity attacker = ((EntityDamageByEntityEvent) entityDamageEvent).getDamager();
|
||||
if (!(attacker instanceof Player))
|
||||
return;
|
||||
|
||||
if (MultiLibUtil.isExternalPlayer(victim))
|
||||
return;
|
||||
|
||||
Vector eventVelocity = event.getVelocity();
|
||||
Vector3d result = onPlayerVelocity(new BukkitPlayer(victim),
|
||||
new Vector3d(eventVelocity.getX(), eventVelocity.getY(), eventVelocity.getZ()));
|
||||
|
||||
if (result != null)
|
||||
event.setVelocity(new Vector(result.getX(), result.getY(), result.getZ()));
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package me.caseload.knockbacksync.listener.bukkit;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* D3 — Resistance Nullification (M1).
|
||||
*
|
||||
* <p>For player-vs-player projectile hits (classified by {@link PvpSource}), forces the
|
||||
* victim's effective {@code generic.knockback_resistance} to 0 for the duration of the
|
||||
* hit by adding a single plugin-owned {@code MULTIPLY_SCALAR_1} {@code -1.0} modifier,
|
||||
* lets vanilla compute full knockback, then removes the owned marker on the next tick.
|
||||
* Equipment and other plugins' modifiers are never read, captured, or removed — only the
|
||||
* owned marker (identified by a constant UUID) is added/removed.</p>
|
||||
*
|
||||
* <p>Melee PvP is owned by {@code LegacyKnockback} (which already ignores resistance when
|
||||
* {@code enable_knockback_resistance=false}) and needs no attribute work; fishing PvP is
|
||||
* owned by OCM's {@code old-fishing-knockback}. Explosions are out of scope on 1.19.4.</p>
|
||||
*
|
||||
* <p>The marker is permanent NMS state until removed, so a leftover marker is reconciled
|
||||
* away on join/quit and on plugin enable/disable (see {@link #cleanupAll()}), guaranteeing
|
||||
* a player is never left permanently with reduced resistance.</p>
|
||||
*/
|
||||
public final class KnockbackResistanceNullifier implements Listener {
|
||||
|
||||
// Constant, plugin-owned modifier identity. Add skips if this UUID is already present;
|
||||
// removal matches by this UUID. Equipment/foreign modifiers are never touched.
|
||||
private static final UUID MODIFIER_UUID = UUID.fromString("7b2a1c84-4d3e-4f0a-9c6b-1e5a9d8c7f30");
|
||||
private static final String MODIFIER_NAME = "knockbacksync_pvp_kb_nullify";
|
||||
|
||||
// Mirrors the previous stripKnockbackResistance guard: if the attribute is unavailable
|
||||
// on this server version, disable for the session and warn once (no per-hit log spam).
|
||||
private static volatile boolean unsupported = false;
|
||||
|
||||
/**
|
||||
* Force the victim's effective knockback resistance to 0 for the current hit by adding the
|
||||
* owned marker, then schedule its removal on the next tick. Idempotent and re-entrancy safe:
|
||||
* if the marker is already present (e.g. a second hit in the same tick) the add is skipped.
|
||||
*
|
||||
* <p>Must be called before vanilla computes the hit's knockback (i.e. during the damage
|
||||
* event at an early priority).</p>
|
||||
*/
|
||||
public static void nullify(Player victim) {
|
||||
final AttributeInstance attribute = resolve(victim);
|
||||
if (attribute == null) return;
|
||||
if (hasOwnedModifier(attribute)) return;
|
||||
|
||||
try {
|
||||
attribute.addModifier(new AttributeModifier(
|
||||
MODIFIER_UUID, MODIFIER_NAME, -1.0, AttributeModifier.Operation.MULTIPLY_SCALAR_1));
|
||||
} catch (IllegalArgumentException alreadyApplied) {
|
||||
return; // raced — modifier already present; nothing to add
|
||||
}
|
||||
|
||||
// Remove on the NEXT tick. Same-event (MONITOR/finally) removal is forbidden: vanilla
|
||||
// computes the knockback after the event returns, so an in-event removal would restore
|
||||
// the attribute before vanilla reads it. On the non-Folia target this is a main-thread
|
||||
// next-tick task; it stays compile-safe (no Player#getScheduler()).
|
||||
Base.INSTANCE.getScheduler().runTaskLater(() -> remove(victim), 1L);
|
||||
}
|
||||
|
||||
/** Remove the owned marker if present. Idempotent; never touches foreign/equipment modifiers. */
|
||||
public static void remove(Player victim) {
|
||||
final AttributeInstance attribute = resolve(victim);
|
||||
if (attribute == null) return;
|
||||
for (AttributeModifier modifier : attribute.getModifiers()) {
|
||||
if (MODIFIER_UUID.equals(modifier.getUniqueId())) {
|
||||
attribute.removeModifier(modifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip the owned marker from every online player (plugin enable/disable reconciliation). */
|
||||
public static void cleanupAll() {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
remove(player);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasOwnedModifier(AttributeInstance attribute) {
|
||||
for (AttributeModifier modifier : attribute.getModifiers()) {
|
||||
if (MODIFIER_UUID.equals(modifier.getUniqueId())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AttributeInstance resolve(Player victim) {
|
||||
if (unsupported || victim == null) return null;
|
||||
try {
|
||||
return victim.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE);
|
||||
} catch (Throwable t) {
|
||||
unsupported = true;
|
||||
Base.INSTANCE.getLogger().warning(
|
||||
"Could not access KNOCKBACK_RESISTANCE attribute; PvP knockback-resistance "
|
||||
+ "nullification disabled for this session: " + t.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── R7 fail-safe: reconcile away any leftover owned marker ──────────────────────────────
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
remove(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
remove(event.getPlayer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package me.caseload.knockbacksync.listener.bukkit;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.projectiles.ProjectileSource;
|
||||
|
||||
/**
|
||||
* D1 — PvP Source Attribution.
|
||||
*
|
||||
* <p>Classifies a damage event on a player victim into a source class so the
|
||||
* knockback-resistance nullifier (D3) knows whether and how to act. Attribution is
|
||||
* self-inclusive: the victim's own projectile (e.g. their own arrow) still counts as
|
||||
* PvP. This classifier only answers "is this player-attributed, and by what source
|
||||
* class?" — it does not modify damage or knockback.</p>
|
||||
*
|
||||
* <p>Scope note (Purpur 1.19.4 target): netherite {@code generic.knockback_resistance}
|
||||
* only reduces knockback applied through {@code LivingEntity.knockback(...)} (melee and
|
||||
* projectile). Explosion knockback is not reduced by it on 1.19.4 (Blast Protection
|
||||
* governs that), and wind charges do not exist, so explosion/wind-charge sources are
|
||||
* classified {@link Type#NONE}.</p>
|
||||
*/
|
||||
public final class PvpSource {
|
||||
|
||||
public enum Type {
|
||||
/** Direct player melee. Owned by {@code LegacyKnockback}; no attribute nullification. */
|
||||
MELEE,
|
||||
/** Player-shot projectile (arrow, spectral arrow, trident, snowball, egg, ender pearl, ...). Nullified by D3. */
|
||||
PROJECTILE,
|
||||
/** Fishing-rod hit (OCM {@code old-fishing-knockback}); owned by the rod sync. No nullification. */
|
||||
FISHING,
|
||||
/** Not attributable to a player (mob, environment, explosion, ...). */
|
||||
NONE
|
||||
}
|
||||
|
||||
private PvpSource() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param event a damage-by-entity event (the caller has not necessarily checked the victim type)
|
||||
* @return the PvP source class; {@link Type#NONE} when the damage is not attributable to a player
|
||||
*/
|
||||
public static Type classify(EntityDamageByEntityEvent event) {
|
||||
if (!(event.getEntity() instanceof Player)) return Type.NONE;
|
||||
final Player victim = (Player) event.getEntity();
|
||||
final Entity damager = event.getDamager();
|
||||
|
||||
// Direct player damager: either a melee hit, or an OCM fishing-rod hit.
|
||||
// old-fishing-knockback deals its damage via damage(victim, rodder), so the
|
||||
// damager is the rodding player (not the bobber). Distinguish the rod hit via
|
||||
// the rod-pending marker KnockbackSync stores for the rod path. Both MELEE and
|
||||
// FISHING are owned elsewhere and get no attribute nullification, so a mislabel
|
||||
// between the two cannot change knockback behaviour.
|
||||
if (damager instanceof Player) {
|
||||
return isRodPending(victim) ? Type.FISHING : Type.MELEE;
|
||||
}
|
||||
|
||||
// Player-shot projectile, resolved generically via ProjectileSource so any
|
||||
// player-shot projectile type is covered without a per-type list (including
|
||||
// types absent from the compile-time API). Self-inclusive: the shooter may be
|
||||
// the victim. A non-player projectile source (e.g. a dispenser's
|
||||
// BlockProjectileSource) or a null shooter is not PvP. Note: the ender pearl's
|
||||
// post-teleport fall damage is a separate non-by-entity event, so it never
|
||||
// reaches this projectile branch.
|
||||
if (damager instanceof Projectile) {
|
||||
final ProjectileSource shooter = ((Projectile) damager).getShooter();
|
||||
return (shooter instanceof Player) ? Type.PROJECTILE : Type.NONE;
|
||||
}
|
||||
|
||||
// Explosions (TNT, end crystal, fireball), mobs, environment: out of scope on 1.19.4.
|
||||
return Type.NONE;
|
||||
}
|
||||
|
||||
private static boolean isRodPending(Player victim) {
|
||||
final User user = new BukkitPlayer(victim).getUser();
|
||||
if (user == null) return false;
|
||||
final PlayerData data = PlayerDataManager.getPlayerData(user);
|
||||
return data != null && data.isRodKnockbackPending();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package me.caseload.knockbacksync.permission;
|
||||
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PluginPermissionChecker implements PermissionChecker {
|
||||
|
||||
// Method getBukkitSenderMethod;
|
||||
//
|
||||
// public PluginPermissionChecker() {
|
||||
// try {
|
||||
// getBukkitSenderMethod = CommandSourceStack.class.getMethod("getBukkitSender");
|
||||
// } catch (NoSuchMethodException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
|
||||
// Currently shouldn't be called
|
||||
@Override
|
||||
public boolean hasPermission(Object nativeType, String s, boolean defaultIfUnset) {
|
||||
if (nativeType instanceof CommandSender) {
|
||||
return ((CommandSender) nativeType).hasPermission(s);
|
||||
}
|
||||
throw new IllegalArgumentException("Attempted to check permission of an object that wasn't a platformplayer or Sender. This should never happen on Bukkit!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Sender source, String s, boolean defaultIfUnset) {
|
||||
return source.hasPermission(s, defaultIfUnset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(PlatformPlayer platformPlayer, String s) {
|
||||
return ((BukkitPlayer) platformPlayer).bukkitPlayer.hasPermission(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package me.caseload.knockbacksync.player;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.protocol.world.BoundingBox;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.google.common.base.Preconditions;
|
||||
import me.caseload.knockbacksync.BukkitBase;
|
||||
import me.caseload.knockbacksync.Platform;
|
||||
import me.caseload.knockbacksync.world.FoliaWorld;
|
||||
import me.caseload.knockbacksync.world.PlatformWorld;
|
||||
import me.caseload.knockbacksync.world.SpigotWorld;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitPlayer implements PlatformPlayer {
|
||||
public final Player bukkitPlayer;
|
||||
public final User user;
|
||||
private String clientBrand = "vanilla";
|
||||
|
||||
// Reflection variables
|
||||
private static Class<?> craftPlayerClass;
|
||||
private static Method getHandleMethod;
|
||||
private static Method getAttackStrengthScaleMethod;
|
||||
private static final ServerVersion currentVersion = PacketEvents.getAPI().getServerManager().getVersion();
|
||||
|
||||
// 1.12.2 support
|
||||
static {
|
||||
try {
|
||||
// Check the current server version
|
||||
|
||||
// If the version is greater than 1.14.4, use the Player method directly
|
||||
if (currentVersion.isOlderThan(ServerVersion.V_1_15)) {
|
||||
Object server = Bukkit.getServer().getClass().getDeclaredMethod("getServer").invoke(Bukkit.getServer());
|
||||
String nmsPackage = server.getClass().getPackage().getName();
|
||||
String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
// Step 1: Load the CraftPlayer class
|
||||
craftPlayerClass = Class.forName(bukkitPackage + ".entity.CraftPlayer");
|
||||
// Step 2: Get the getHandle method
|
||||
getHandleMethod = craftPlayerClass.getMethod("getHandle");
|
||||
// Step 3: Get the getAttackStrengthScale method from the EntityPlayer class
|
||||
Class<?> entityPlayerClass = Class.forName(nmsPackage + ".EntityPlayer");
|
||||
String getAttackStrengthScaleMethodName = "";
|
||||
|
||||
// Determine the method name based on the version
|
||||
if (currentVersion.isOlderThan(ServerVersion.V_1_13)) {
|
||||
getAttackStrengthScaleMethodName = "n";
|
||||
} else if (currentVersion.isOlderThan(ServerVersion.V_1_14)) {
|
||||
getAttackStrengthScaleMethodName = "r";
|
||||
} else if (currentVersion.isOlderThan(ServerVersion.V_1_15)) {
|
||||
getAttackStrengthScaleMethodName = "s";
|
||||
}
|
||||
|
||||
// Get the attack strength scale method for EntityPlayer
|
||||
getAttackStrengthScaleMethod = entityPlayerClass.getMethod(getAttackStrengthScaleMethodName, float.class);
|
||||
getAttackStrengthScaleMethod.setAccessible(true);
|
||||
}
|
||||
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
||||
throw new IllegalStateException("Method of Class required to support this version not found via reflection" + e);
|
||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access required methods via reflection to support this version" + e);
|
||||
}
|
||||
}
|
||||
|
||||
public BukkitPlayer(Player player) {
|
||||
Preconditions.checkArgument(player != null);
|
||||
this.bukkitPlayer = player;
|
||||
this.user = PacketEvents.getAPI().getPlayerManager().getUser(bukkitPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return bukkitPlayer.getUniqueId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return bukkitPlayer.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getX() {
|
||||
return bukkitPlayer.getLocation().getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getY() {
|
||||
return bukkitPlayer.getLocation().getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getZ() {
|
||||
return bukkitPlayer.getLocation().getZ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPitch() {
|
||||
return bukkitPlayer.getLocation().getPitch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getYaw() {
|
||||
return bukkitPlayer.getLocation().getYaw();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnGround() {
|
||||
/* Inconsistent with Entity.isOnGround()
|
||||
/ Checks to see if this player is currently standing on a block.
|
||||
/ This information may not be reliable, as it is a state provided by the client, and may therefore not be accurate.
|
||||
/ It can also easily be spoofed. We may want to cast to LivingEntity and call isOnGround() instead
|
||||
*/
|
||||
return bukkitPlayer.isOnGround();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getPing() {
|
||||
if (currentVersion.isNewerThanOrEquals(ServerVersion.V_1_16_5)) {
|
||||
return bukkitPlayer.getPing();
|
||||
} else {
|
||||
return PacketEvents.getAPI().getPlayerManager().getPing(bukkitPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGliding() {
|
||||
return bukkitPlayer.isGliding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformWorld getWorld() {
|
||||
return BukkitBase.INSTANCE.getPlatform() == Platform.FOLIA ? new FoliaWorld(bukkitPlayer.getWorld()): new SpigotWorld(bukkitPlayer.getWorld());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d getLocation() {
|
||||
org.bukkit.Location location = bukkitPlayer.getLocation();
|
||||
return new Vector3d(location.getX(), location.getY(), location.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull String s) {
|
||||
bukkitPlayer.sendMessage(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getAttackCooldown() {
|
||||
if (currentVersion.isNewerThan(ServerVersion.V_1_14_4)) {
|
||||
return bukkitPlayer.getAttackCooldown();
|
||||
} else {
|
||||
try {
|
||||
// Step 1: Get the CraftPlayer instance
|
||||
// Step 2: Get the handle (NMS EntityPlayer)
|
||||
Object entityPlayer = getHandleMethod.invoke(bukkitPlayer);
|
||||
// Step 3: Invoke the getAttackStrengthScale method
|
||||
return (float) getAttackStrengthScaleMethod.invoke(entityPlayer, 0.5f);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("This plugin will not work. NMS mapping for getAttackCooldown() failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSprinting() {
|
||||
return bukkitPlayer.isSprinting();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMainHandKnockbackLevel() {
|
||||
return bukkitPlayer.getInventory().getItemInMainHand().getEnchantmentLevel(Enchantment.KNOCKBACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWeaponKnockbackLevel() {
|
||||
// OCM parity: use the main hand item, or the off hand when the main hand is empty.
|
||||
org.bukkit.inventory.ItemStack mainHand = bukkitPlayer.getInventory().getItemInMainHand();
|
||||
org.bukkit.inventory.ItemStack weapon = mainHand.getType() == org.bukkit.Material.AIR
|
||||
? bukkitPlayer.getInventory().getItemInOffHand()
|
||||
: mainHand;
|
||||
return weapon.getEnchantmentLevel(Enchantment.KNOCKBACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Integer getNoDamageTicks() {
|
||||
return bukkitPlayer.getNoDamageTicks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVelocity(Vector3d adjustedVelocity) {
|
||||
bukkitPlayer.setVelocity(new Vector(adjustedVelocity.x, adjustedVelocity.y, adjustedVelocity.z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3d getVelocity() {
|
||||
final Vector bukkitVelocity = bukkitPlayer.getVelocity();
|
||||
return new Vector3d(bukkitVelocity.getX(), bukkitVelocity.getY(), bukkitVelocity.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getJumpPower() {
|
||||
double jumpVelocity = 0.42;
|
||||
|
||||
PotionEffect jumpEffect = bukkitPlayer.getPotionEffect(PotionEffectType.JUMP);
|
||||
if (jumpEffect != null) {
|
||||
int amplifier = jumpEffect.getAmplifier();
|
||||
jumpVelocity += (amplifier + 1) * 0.1F;
|
||||
}
|
||||
|
||||
return jumpVelocity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundingBox getBoundingBox() {
|
||||
org.bukkit.util.BoundingBox boundingBox = bukkitPlayer.getBoundingBox();
|
||||
return new BoundingBox(boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMinZ(), boundingBox.getMaxX(), boundingBox.getMaxY(), boundingBox.getMaxZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable User getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientBrand(String brand) {
|
||||
this.clientBrand = brand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientBrand() {
|
||||
return this.clientBrand;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitScheduler;
|
||||
|
||||
public class BukkitSchedulerAdapter implements SchedulerAdapter {
|
||||
private final Plugin plugin;
|
||||
private final BukkitScheduler scheduler;
|
||||
|
||||
public BukkitSchedulerAdapter(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.scheduler = Bukkit.getScheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTask(Runnable task) {
|
||||
return new BukkitTaskHandle(scheduler.runTask(plugin, task));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskAsynchronously(Runnable task) {
|
||||
return new BukkitTaskHandle(scheduler.runTaskAsynchronously(plugin, task));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
|
||||
return new BukkitTaskHandle(scheduler.runTaskLater(plugin, task, delayTicks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks) {
|
||||
return new BukkitTaskHandle(scheduler.runTaskTimer(plugin, task, delayTicks, periodTicks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay) {
|
||||
return new BukkitTaskHandle(scheduler.runTaskLaterAsynchronously(plugin, task, delay));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period) {
|
||||
return new BukkitTaskHandle(scheduler.runTaskTimerAsynchronously(plugin, task, delay, period));
|
||||
}
|
||||
|
||||
// Bukkit should take care of this for us automatically
|
||||
@Override
|
||||
public void shutdown() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class BukkitTaskHandle implements AbstractTaskHandle {
|
||||
private final BukkitTask bukkitTask;
|
||||
private boolean cancelled;
|
||||
|
||||
public BukkitTaskHandle(@NotNull BukkitTask bukkitTask) {
|
||||
this.bukkitTask = bukkitTask;
|
||||
}
|
||||
|
||||
public Plugin getOwner() {
|
||||
return this.bukkitTask.getOwner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getCancelled() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.bukkitTask.cancel();
|
||||
this.cancelled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
import io.github.retrooper.packetevents.util.folia.FoliaScheduler;
|
||||
import io.github.retrooper.packetevents.util.folia.GlobalRegionScheduler;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class FoliaSchedulerAdapter implements SchedulerAdapter {
|
||||
private final Plugin plugin;
|
||||
|
||||
public FoliaSchedulerAdapter(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTask(Runnable task) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getGlobalRegionScheduler().run(plugin, scheduledTask -> task.run()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskAsynchronously(Runnable task) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getAsyncScheduler().runNow(plugin, scheduledTask -> task.run()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> task.run(), delayTicks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getAsyncScheduler().runDelayed(plugin, scheduledTask -> task.run(), delay * 50, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period) {
|
||||
return new FoliaTaskHandle(FoliaScheduler.getAsyncScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delay, period));
|
||||
}
|
||||
|
||||
// Folia takes care of this
|
||||
@Override
|
||||
public void shutdown() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
import io.github.retrooper.packetevents.util.folia.TaskWrapper;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
public class FoliaTaskHandle implements AbstractTaskHandle {
|
||||
|
||||
private final TaskWrapper scheduledTask; // Store as Object instead of ScheduledTask
|
||||
private boolean cancelled;
|
||||
|
||||
public FoliaTaskHandle(@NotNull TaskWrapper scheduledTask) {
|
||||
this.scheduledTask = scheduledTask;
|
||||
}
|
||||
|
||||
public Plugin getOwner() {
|
||||
return scheduledTask.getOwner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getCancelled() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
scheduledTask.cancel();
|
||||
this.cancelled = true;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.command.generic.PlayerSelector;
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
public class BukkitPlayerSelectorAdapter implements PlayerSelector {
|
||||
private final org.incendo.cloud.bukkit.data.SinglePlayerSelector bukkitSelector;
|
||||
|
||||
public BukkitPlayerSelectorAdapter(org.incendo.cloud.bukkit.data.SinglePlayerSelector bukkitSelector) {
|
||||
this.bukkitSelector = bukkitSelector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformPlayer getSinglePlayer() {
|
||||
return new BukkitPlayer(bukkitSelector.single());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PlatformPlayer> getPlayers() {
|
||||
return Collections.singletonList(new BukkitPlayer(bukkitSelector.single()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String inputString() {
|
||||
return bukkitSelector.inputString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.command.generic.AbstractPlayerSelectorParser;
|
||||
import me.caseload.knockbacksync.command.generic.PlayerSelector;
|
||||
import org.incendo.cloud.bukkit.parser.selector.SinglePlayerSelectorParser;
|
||||
import org.incendo.cloud.context.CommandContext;
|
||||
import org.incendo.cloud.parser.ParserDescriptor;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitPlayerSelectorParser<C> extends AbstractPlayerSelectorParser<C> {
|
||||
|
||||
@Override
|
||||
public ParserDescriptor<C, PlayerSelector> descriptor() {
|
||||
return createDescriptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParserDescriptor<C, ?> getPlatformSpecificDescriptor() {
|
||||
return SinglePlayerSelectorParser.singlePlayerSelectorParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<PlayerSelector> adaptToCommonSelector(CommandContext<C> context, Object platformSpecificSelector) {
|
||||
return CompletableFuture.completedFuture(new BukkitPlayerSelectorAdapter((org.incendo.cloud.bukkit.data.SinglePlayerSelector) platformSpecificSelector));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.BukkitBase;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.command.RemoteConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.incendo.cloud.SenderMapper;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitSenderFactory extends SenderFactory<BukkitBase, CommandSender> implements SenderMapper<CommandSender, Sender> {
|
||||
// private final BukkitAudiences audiences;
|
||||
|
||||
public BukkitSenderFactory(BukkitBase kbSyncBukkitBase) {
|
||||
super(kbSyncBukkitBase);
|
||||
// this.audiences = BukkitAudiences.create(kbSyncBukkitBase.getLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getName(CommandSender sender) {
|
||||
if (sender instanceof Player) {
|
||||
return sender.getName();
|
||||
}
|
||||
return Sender.CONSOLE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UUID getUniqueId(CommandSender sender) {
|
||||
if (sender instanceof Player) {
|
||||
return ((Player) sender).getUniqueId();
|
||||
}
|
||||
return Sender.CONSOLE_UUID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendMessage(CommandSender sender, String message) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
|
||||
// @Override
|
||||
// protected void sendMessage(CommandSender sender, Component message) {
|
||||
// // we can safely send async for players and the console - otherwise, send it sync
|
||||
// if (sender instanceof Player || sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) {
|
||||
// this.audiences.sender(sender).sendMessage(message);
|
||||
// } else {
|
||||
// getPlugin().getBootstrap().getScheduler().executeSync(() -> this.audiences.sender(sender).sendMessage(message));
|
||||
// }
|
||||
// }
|
||||
|
||||
@Override
|
||||
protected boolean hasPermission(CommandSender sender, String node) {
|
||||
return sender.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasPermission(CommandSender sender, String node, boolean defaultIfUnset) {
|
||||
return sender.hasPermission(new Permission(node, defaultIfUnset ? PermissionDefault.TRUE : PermissionDefault.FALSE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performCommand(CommandSender sender, String command) {
|
||||
//
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isConsole(CommandSender sender) {
|
||||
return sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Sender map(@NonNull CommandSender base) {
|
||||
return this.wrap(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CommandSender reverse(@NonNull Sender mapped) {
|
||||
return this.unwrap(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public class BukkitStatsManager extends StatsManager {
|
||||
|
||||
public BukkitStatsManager(Plugin plugin) {
|
||||
super(new MetricsBukkit(plugin, 23568));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
|
||||
/*
|
||||
* This Metrics class was auto-generated and can be copied into your project if you are
|
||||
* not using a build tool like Gradle or Maven for dependency management.
|
||||
*
|
||||
* IMPORTANT: You are not allowed to modify this class, except changing the package.
|
||||
*
|
||||
* Disallowed modifications include but are not limited to:
|
||||
* - Remove the option for users to opt-out
|
||||
* - Change the frequency for data submission
|
||||
* - Obfuscate the code (every obfuscator should allow you to make an exception for specific files)
|
||||
* - Reformat the code (if you use a linter, add an exception)
|
||||
*
|
||||
* Violations will result in a ban of your plugin and account from bStats.
|
||||
*/
|
||||
|
||||
import me.caseload.knockbacksync.BukkitLoaderPlugin;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.stats.CustomChart;
|
||||
import me.caseload.knockbacksync.stats.JsonObjectBuilder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MetricsBukkit implements Metrics {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final MetricsBase metricsBase;
|
||||
|
||||
|
||||
public MetricsBukkit(Plugin plugin, int serviceId) {
|
||||
this.plugin = plugin;
|
||||
// Get the config file
|
||||
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
|
||||
File configFile = new File(bStatsFolder, "config.yml");
|
||||
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
|
||||
if (!config.isSet("serverUuid")) {
|
||||
config.addDefault("enabled", true);
|
||||
config.addDefault("serverUuid", UUID.randomUUID().toString());
|
||||
config.addDefault("logFailedRequests", false);
|
||||
config.addDefault("logSentData", false);
|
||||
config.addDefault("logResponseStatusText", false);
|
||||
// Inform the server owners about bStats
|
||||
config
|
||||
.options()
|
||||
.header(
|
||||
"bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
|
||||
+ "many people use their plugin and their total player count. It's recommended to keep bStats\n"
|
||||
+ "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
|
||||
+ "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
|
||||
+ "anonymous.")
|
||||
.copyDefaults(true);
|
||||
try {
|
||||
config.save(configFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
// Load the data
|
||||
boolean enabled = config.getBoolean("enabled", true);
|
||||
String serverUUID = config.getString("serverUuid");
|
||||
boolean logErrors = config.getBoolean("logFailedRequests", false);
|
||||
boolean logSentData = config.getBoolean("logSentData", false);
|
||||
boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
|
||||
// boolean isFolia = false;
|
||||
// try {
|
||||
// isFolia = Class.forName("io.papermc.paper.threadedregions.RegionizedServer") != null;
|
||||
// } catch (Exception e) {
|
||||
// }
|
||||
metricsBase =
|
||||
new // See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
// See https://github.com/Bastian/bstats-metrics/pull/126
|
||||
MetricsBase(
|
||||
"bukkit",
|
||||
serverUUID,
|
||||
serviceId,
|
||||
enabled,
|
||||
this::appendPlatformData,
|
||||
this::appendServiceData,
|
||||
submitDataTask -> Base.INSTANCE.getScheduler().runTask(submitDataTask),
|
||||
// isFolia
|
||||
// ? null
|
||||
// : submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
|
||||
plugin::isEnabled,
|
||||
(message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
|
||||
(message) -> this.plugin.getLogger().log(Level.INFO, message),
|
||||
logErrors,
|
||||
logSentData,
|
||||
logResponseStatusText,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the underlying scheduler service.
|
||||
*/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
metricsBase.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom chart.
|
||||
*
|
||||
* @param chart The chart to add.
|
||||
*/
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
metricsBase.addCustomChart(chart);
|
||||
}
|
||||
|
||||
private void appendPlatformData(JsonObjectBuilder builder) {
|
||||
builder.appendField("playerAmount", getPlayerAmount());
|
||||
builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
|
||||
builder.appendField("bukkitVersion", Bukkit.getVersion());
|
||||
builder.appendField("bukkitName", Bukkit.getName());
|
||||
builder.appendField("javaVersion", System.getProperty("java.version"));
|
||||
builder.appendField("osName", System.getProperty("os.name"));
|
||||
builder.appendField("osArch", System.getProperty("os.arch"));
|
||||
builder.appendField("osVersion", System.getProperty("os.version"));
|
||||
builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
private void appendServiceData(JsonObjectBuilder builder) {
|
||||
builder.appendField("pluginVersion", plugin.getDescription().getVersion());
|
||||
}
|
||||
|
||||
private int getPlayerAmount() {
|
||||
try {
|
||||
// Around MC 1.8 the return type was changed from an array to a collection,
|
||||
// This fixes java.lang.NoSuchMethodError:
|
||||
// org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
|
||||
Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
|
||||
return onlinePlayersMethod.getReturnType().equals(Collection.class)
|
||||
? ((Collection<?>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
|
||||
: ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
|
||||
} catch (Exception e) {
|
||||
// Just use the new method if the reflection failed
|
||||
return Bukkit.getOnlinePlayers().size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MultiLibUtil {
|
||||
|
||||
public final static Method externalPlayerMethod = getMethod(Player.class, "isExternalPlayer");
|
||||
|
||||
public static Method getMethod(Class<?> clazz, String methodName) {
|
||||
try {
|
||||
return clazz.getMethod(methodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: cache external players for better performance, but this only matters for people using multi-lib
|
||||
public static boolean isExternalPlayer(Player player) {
|
||||
if (externalPlayerMethod == null || (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_18)))
|
||||
return false;
|
||||
try {
|
||||
return (boolean) externalPlayerMethod.invoke(player);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package me.caseload.knockbacksync.world;
|
||||
|
||||
import me.caseload.knockbacksync.player.BukkitPlayer;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitServer implements PlatformServer {
|
||||
public Collection<PlatformPlayer> getOnlinePlayers() {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
.map(BukkitPlayer::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformPlayer getPlayer(UUID uuid) {
|
||||
return new BukkitPlayer(Bukkit.getPlayer(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformPlayer getPlayer(Object nativePlatformPlayer) {
|
||||
return new BukkitPlayer((Player) nativePlatformPlayer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package me.caseload.knockbacksync.world;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
|
||||
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.BlockState;
|
||||
|
||||
public class FoliaWorld extends SpigotWorld {
|
||||
public FoliaWorld(World world) {
|
||||
super(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedBlockState getBlockStateAt(int x, int y, int z) {
|
||||
BlockState block = this.world.getBlockState(x, y, z);
|
||||
return SpigotConversionUtil.fromBukkitBlockData(block.getBlockData());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package me.caseload.knockbacksync.world;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.protocol.world.BlockFace;
|
||||
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.util.Vector3i;
|
||||
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
|
||||
import me.caseload.knockbacksync.world.raytrace.FluidHandling;
|
||||
import me.caseload.knockbacksync.world.raytrace.RayTraceResult;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class SpigotWorld implements PlatformWorld {
|
||||
protected final World world;
|
||||
|
||||
// Reflection variables
|
||||
private static Class<?> craftWorldClass;
|
||||
private static Class<?> vec3DClass;
|
||||
private static Class<?> movingObjectPositionClass;
|
||||
private static Class<?> enumDirectionClass;
|
||||
private static Method craftWorldGetHandleMethod;
|
||||
private static Method rayTraceMethod;
|
||||
private static Method vec3DAddMethod;
|
||||
|
||||
static {
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13)) {
|
||||
try {
|
||||
Object server = Bukkit.getServer().getClass().getDeclaredMethod("getServer").invoke(Bukkit.getServer());
|
||||
String nmsPackage = server.getClass().getPackage().getName();
|
||||
String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
// Load necessary classes dynamically for Minecraft 1.12.2
|
||||
craftWorldClass = Class.forName(bukkitPackage + ".CraftWorld");
|
||||
vec3DClass = Class.forName(nmsPackage + ".Vec3D");
|
||||
movingObjectPositionClass = Class.forName(nmsPackage + ".MovingObjectPosition");
|
||||
enumDirectionClass = Class.forName(nmsPackage + ".EnumDirection");
|
||||
|
||||
// CraftWorld
|
||||
|
||||
// Get method
|
||||
craftWorldGetHandleMethod = craftWorldClass.getMethod("getHandle");
|
||||
rayTraceMethod = Class.forName(nmsPackage + ".World").getMethod("rayTrace", vec3DClass, vec3DClass, boolean.class, boolean.class, boolean.class);
|
||||
vec3DAddMethod = vec3DClass.getMethod("add", double.class, double.class, double.class);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SpigotWorld(World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
private static BlockFace getBlockFaceFrom(org.bukkit.block.BlockFace direction) {
|
||||
switch (direction) {
|
||||
case NORTH:
|
||||
return BlockFace.NORTH;
|
||||
case SOUTH:
|
||||
return BlockFace.SOUTH;
|
||||
case EAST:
|
||||
return BlockFace.EAST;
|
||||
case WEST:
|
||||
return BlockFace.WEST;
|
||||
case UP:
|
||||
return BlockFace.UP;
|
||||
case DOWN:
|
||||
return BlockFace.DOWN;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedBlockState getBlockStateAt(int x, int y, int z) {
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
return SpigotConversionUtil.fromBukkitBlockData(block.getBlockData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedBlockState getBlockStateAt(Vector3d loc) {
|
||||
return getBlockStateAt((int) Math.floor(loc.x), (int) Math.floor(loc.y), (int) Math.floor(loc.z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RayTraceResult rayTraceBlocks(Vector3d start, Vector3d direction, double maxDistance, FluidHandling fluidHandling, boolean ignorePassableBlocks) {
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThan(ServerVersion.V_1_12_2)) {
|
||||
Vector startVec = new Vector(start.getX(), start.getY(), start.getZ());
|
||||
Vector directionVec = new Vector(direction.getX(), direction.getY(), direction.getZ());
|
||||
|
||||
FluidCollisionMode fluidMode = (fluidHandling == FluidHandling.NONE) ? FluidCollisionMode.NEVER :
|
||||
(fluidHandling == FluidHandling.SOURCE_ONLY) ? FluidCollisionMode.SOURCE_ONLY :
|
||||
FluidCollisionMode.ALWAYS;
|
||||
|
||||
org.bukkit.util.RayTraceResult result = world.rayTraceBlocks(startVec.toLocation(world), directionVec, maxDistance, fluidMode, ignorePassableBlocks);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
return new RayTraceResult(
|
||||
new Vector3d(result.getHitPosition().getX(), result.getHitPosition().getY(), result.getHitPosition().getZ()),
|
||||
getBlockFaceFrom(result.getHitBlockFace()),
|
||||
new Vector3i(result.getHitBlock().getX(), result.getHitBlock().getY(), result.getHitBlock().getZ()),
|
||||
result.getHitBlock() != null ? SpigotConversionUtil.fromBukkitBlockData(result.getHitBlock().getBlockData()) : null
|
||||
);
|
||||
// Only tested on 1.12.2
|
||||
} else {
|
||||
try {
|
||||
// Create start and end positions
|
||||
Object startPos = vec3DClass.getConstructor(double.class, double.class, double.class)
|
||||
.newInstance(start.getX(), start.getY(), start.getZ());
|
||||
Object endPos = vec3DAddMethod.invoke(startPos, direction.getX(), direction.getY(), direction.getZ());
|
||||
|
||||
// Call rayTrace method
|
||||
Object hitResult = rayTraceMethod.invoke(craftWorldGetHandleMethod.invoke(world), startPos, endPos, false, true, false);
|
||||
if (hitResult == null) return null;
|
||||
|
||||
// Extract hit position and direction
|
||||
Vector3d hitPosition = new Vector3d(
|
||||
(double) hitResult.getClass().getField("pos").get(hitResult),
|
||||
(double) hitResult.getClass().getField("pos").get(hitResult).getClass().getField("y").get(hitResult.getClass().getField("pos").get(hitResult)),
|
||||
(double) hitResult.getClass().getField("pos").get(hitResult).getClass().getField("z").get(hitResult.getClass().getField("pos").get(hitResult))
|
||||
);
|
||||
|
||||
Object hitDirection = hitResult.getClass().getField("direction").get(hitResult);
|
||||
Object hitBlock = hitResult.getClass().getField("e").get(hitResult); // e = BlockHitResult
|
||||
|
||||
return new RayTraceResult(
|
||||
hitPosition,
|
||||
getHitBlockFace(hitDirection),
|
||||
new Vector3i((int) hitBlock.getClass().getField("x").get(hitBlock),
|
||||
(int) hitBlock.getClass().getField("y").get(hitBlock),
|
||||
(int) hitBlock.getClass().getField("z").get(hitBlock)),
|
||||
hitBlock != null ? WrappedBlockState.getByString(hitBlock.getClass().getField("type").get(hitBlock).toString()) : null
|
||||
);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null; // Return null if there was an error during ray tracing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockFace getHitBlockFace(Object enumDirection) {
|
||||
try {
|
||||
// Match the EnumDirection to the corresponding BlockFace
|
||||
if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("NORTH").get(null))) {
|
||||
return BlockFace.NORTH;
|
||||
} else if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("SOUTH").get(null))) {
|
||||
return BlockFace.SOUTH;
|
||||
} else if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("EAST").get(null))) {
|
||||
return BlockFace.EAST;
|
||||
} else if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("WEST").get(null))) {
|
||||
return BlockFace.WEST;
|
||||
} else if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("UP").get(null))) {
|
||||
return BlockFace.UP;
|
||||
} else if (enumDirection.equals(SpigotWorld.enumDirectionClass.getField("DOWN").get(null))) {
|
||||
return BlockFace.DOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
throw new IllegalStateException("Unexpected value: " + enumDirection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
name: KnockbackSync-OG
|
||||
version: ${version}
|
||||
main: me.caseload.knockbacksync.BukkitLoaderPlugin
|
||||
api-version: '1.13' # We actually support 1.12.2, api-version was added later though
|
||||
authors: [ Caseload, Axionize ]
|
||||
description: Synchronizes player knockback for smoother gameplay.
|
||||
folia-supported: true
|
||||
depend: ${depend}
|
||||
permissions:
|
||||
knockbacksync.update:
|
||||
default: op
|
||||
knockbacksync.toggle.global:
|
||||
default: op
|
||||
knockbacksync.toggle.other:
|
||||
default: op
|
||||
knockbacksync.toggle.self:
|
||||
default: op
|
||||
knockbacksync.ping:
|
||||
default: op
|
||||
knockbacksync.reload:
|
||||
default: op
|
||||
knockbacksync.toggleoffground:
|
||||
default: op
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id("com.github.gmazzo.buildconfig") version "3.1.0"
|
||||
}
|
||||
|
||||
val shadePE: Boolean by rootProject.extra
|
||||
|
||||
dependencies {
|
||||
// True compileOnly deps
|
||||
compileOnly("org.geysermc.floodgate:api:2.0-SNAPSHOT")
|
||||
compileOnly("io.netty:netty-all:4.1.72.Final")
|
||||
compileOnly("org.projectlombok:lombok:1.18.34")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.34")
|
||||
|
||||
// Upstream 2.11.2 API; runtime impl is the externally installed True-OG fork or the shaded bundle.
|
||||
if (shadePE) {
|
||||
implementation("com.github.retrooper:packetevents-api:2.11.2")
|
||||
} else {
|
||||
compileOnly("com.github.retrooper:packetevents-api:2.11.2")
|
||||
}
|
||||
|
||||
implementation("org.yaml:snakeyaml:2.0")
|
||||
implementation("org.kohsuke:github-api:1.326") {
|
||||
exclude(group = "commons-io", module = "commons-io")
|
||||
exclude(group = "org.apache.commons", module = "commons-lang3")
|
||||
}
|
||||
|
||||
implementation("org.incendo:cloud-core:2.0.0")
|
||||
implementation("org.incendo:cloud-minecraft-extras:2.0.0-beta.10")
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
buildConfigField("String", "GITHUB_REPO", "\"${project.rootProject.ext["githubRepo"]}\"")
|
||||
// Exposes shadePE to source code as a compile-time constant.
|
||||
buildConfigField("boolean", "SHADE_PE", shadePE.toString())
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package me.caseload.knockbacksync;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerCommon;
|
||||
import lombok.Getter;
|
||||
import me.caseload.knockbacksync.command.MainCommand;
|
||||
import me.caseload.knockbacksync.common.BuildConfig;
|
||||
import me.caseload.knockbacksync.command.generic.AbstractPlayerSelectorParser;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.command.subcommand.PingCommand;
|
||||
import me.caseload.knockbacksync.command.subcommand.ReloadCommand;
|
||||
import me.caseload.knockbacksync.command.subcommand.StatusCommand;
|
||||
import me.caseload.knockbacksync.command.subcommand.ToggleCommand;
|
||||
import me.caseload.knockbacksync.command.subcommand.ToggleOffGroundSubcommand;
|
||||
import me.caseload.knockbacksync.event.Event;
|
||||
import me.caseload.knockbacksync.event.EventBus;
|
||||
import me.caseload.knockbacksync.event.OptimizedEventBus;
|
||||
import me.caseload.knockbacksync.listener.packetevents.*;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.permission.PermissionChecker;
|
||||
import me.caseload.knockbacksync.scheduler.SchedulerAdapter;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.stats.custom.ClientBrandsPie;
|
||||
import me.caseload.knockbacksync.stats.custom.PluginJarHashProvider;
|
||||
import me.caseload.knockbacksync.stats.custom.StatsManager;
|
||||
import me.caseload.knockbacksync.world.PlatformServer;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.kohsuke.github.GitHub;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
// Base class
|
||||
public abstract class Base {
|
||||
public static Logger LOGGER;
|
||||
public static Base INSTANCE;
|
||||
|
||||
@Getter private final Platform platform;
|
||||
@Getter protected StatsManager statsManager;
|
||||
@Getter protected PlatformServer platformServer;
|
||||
@Getter protected PluginJarHashProvider pluginJarHashProvider;
|
||||
@Getter protected SchedulerAdapter scheduler;
|
||||
@Getter protected ConfigManager configManager;
|
||||
@Getter protected CommandManager<Sender> commandManager;
|
||||
@Getter protected final EventBus eventBus = new OptimizedEventBus();
|
||||
|
||||
@Getter
|
||||
protected AbstractPlayerSelectorParser<Sender> playerSelectorParser;
|
||||
|
||||
// Tracked so onDisable can unregister exactly the listeners registered against the shared provider.
|
||||
private final java.util.List<PacketListenerCommon> peListenerHandles = new java.util.ArrayList<>();
|
||||
|
||||
protected Base() {
|
||||
this.platform = detectPlatform();
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
private Platform detectPlatform() {
|
||||
final Map<String, Platform> platforms = Collections.unmodifiableMap(new HashMap<String, Platform>() {{
|
||||
put("io.papermc.paper.threadedregions.RegionizedServer", Platform.FOLIA);
|
||||
put("org.bukkit.Bukkit", Platform.BUKKIT);
|
||||
put("net.fabricmc.loader.api.FabricLoader", Platform.FABRIC);
|
||||
}});
|
||||
|
||||
return platforms.entrySet().stream()
|
||||
.filter(entry -> isClassPresent(entry.getKey()))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Unknown platform!"));
|
||||
}
|
||||
|
||||
private boolean isClassPresent(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Logger getLogger();
|
||||
|
||||
public abstract File getDataFolder();
|
||||
|
||||
public abstract InputStream getResource(String filename);
|
||||
|
||||
public abstract void load();
|
||||
|
||||
public void enable() {
|
||||
LOGGER = getLogger();
|
||||
saveDefaultConfig();
|
||||
initializePacketEvents();
|
||||
registerCommonListeners();
|
||||
registerPlatformListeners();
|
||||
registerCommands();
|
||||
initializeScheduler();
|
||||
configManager.loadConfig(false);
|
||||
statsManager.init();
|
||||
checkForUpdates();
|
||||
}
|
||||
|
||||
public abstract void initializeScheduler();
|
||||
|
||||
public void initializePacketEvents() {
|
||||
// External-provider mode skips lifecycle ownership entirely.
|
||||
if (!BuildConfig.SHADE_PE) {
|
||||
return;
|
||||
}
|
||||
PacketEvents.getAPI().getSettings()
|
||||
.checkForUpdates(false)
|
||||
.debug(false);
|
||||
|
||||
PacketEvents.getAPI().init();
|
||||
}
|
||||
|
||||
protected void registerCommonListeners() {
|
||||
registerTrackedPacketListener(new AttributeChangeListener());
|
||||
registerTrackedPacketListener(new PingSendListener());
|
||||
registerTrackedPacketListener(new PingReceiveListener());
|
||||
registerTrackedPacketListener(new PacketPlayerJoinQuit());
|
||||
registerTrackedPacketListener(new ClientBrandListener());
|
||||
Event.setEventBus(eventBus);
|
||||
}
|
||||
|
||||
/** Register a PacketEvents listener and remember its handle for unregister on disable. */
|
||||
protected void registerTrackedPacketListener(PacketListenerCommon listener) {
|
||||
PacketEvents.getAPI().getEventManager().registerListener(listener);
|
||||
peListenerHandles.add(listener);
|
||||
}
|
||||
|
||||
/** Unregister every tracked PacketEvents listener. */
|
||||
public void unregisterPacketListeners() {
|
||||
for (PacketListenerCommon handle : peListenerHandles) {
|
||||
try {
|
||||
PacketEvents.getAPI().getEventManager().unregisterListener(handle);
|
||||
} catch (Throwable t) {
|
||||
if (LOGGER != null) {
|
||||
LOGGER.warning("Failed to unregister PE listener: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
peListenerHandles.clear();
|
||||
}
|
||||
|
||||
protected abstract void registerPlatformListeners();
|
||||
|
||||
protected void registerCommands() {
|
||||
List<BuilderCommand> list = Arrays.asList(
|
||||
new MainCommand(),
|
||||
new ReloadCommand(),
|
||||
new PingCommand(),
|
||||
new StatusCommand(),
|
||||
new ToggleOffGroundSubcommand(),
|
||||
new ToggleCommand()
|
||||
);
|
||||
list.forEach(command -> command.register(commandManager));
|
||||
}
|
||||
|
||||
protected abstract String getVersion();
|
||||
|
||||
protected void checkForUpdates() {
|
||||
getLogger().info("Checking for updates...");
|
||||
|
||||
scheduler.runTaskAsynchronously(() -> {
|
||||
try {
|
||||
GitHub github = GitHub.connectAnonymously();
|
||||
String latestVersion = github.getRepository("CASELOAD7000/knockback-sync")
|
||||
.getLatestRelease()
|
||||
.getTagName();
|
||||
|
||||
String currentVersion = getVersion();
|
||||
|
||||
int comparisonResult = compareVersions(currentVersion, latestVersion);
|
||||
|
||||
if (comparisonResult < 0) {
|
||||
LOGGER.warning("You are running an older version. A new update is available for download at: https://github.com/CASELOAD7000/knockback-sync/releases/latest");
|
||||
configManager.setUpdateAvailable(true);
|
||||
} else if (comparisonResult > 0) {
|
||||
if (currentVersion.contains("-dev"))
|
||||
LOGGER.info("You are running a development build newer than the latest release.");
|
||||
else {
|
||||
LOGGER.info("You are running a version newer than the latest release.");
|
||||
}
|
||||
} else {
|
||||
LOGGER.info("You are running the latest release.");
|
||||
}
|
||||
|
||||
if (configManager.isUpdateAvailable() && configManager.isAutoUpdate()) {
|
||||
LOGGER.info("Updating...");
|
||||
byte[] bytes = downloadLatestRelease(github);
|
||||
updatePlugin(bytes);
|
||||
|
||||
if (hasRestartScript())
|
||||
scheduler.runTask(this::restartServer);
|
||||
else {
|
||||
// Yeah
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to check for updates: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePlugin(byte[] pluginBytes) throws URISyntaxException {
|
||||
File file = new File(getJarURL().toURI());
|
||||
|
||||
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(pluginBytes);
|
||||
LOGGER.info("Successfully updated the plugin!");
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to update: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] downloadLatestRelease(GitHub github) {
|
||||
try {
|
||||
return github.getRepository("CASELOAD7000/knockback-sync")
|
||||
.getLatestRelease()
|
||||
.getAssets().stream()
|
||||
.filter(asset -> asset.getName().endsWith(".jar")
|
||||
&& asset.getName().contains(platform.equals(Platform.FABRIC)
|
||||
? "fabric" : "bukkit"))
|
||||
.findFirst()
|
||||
.map(asset -> {
|
||||
try (InputStream inputStream = new URL(asset.getBrowserDownloadUrl()).openStream()) {
|
||||
inputStream.reset();
|
||||
byte[] bytes = new byte[inputStream.available()];
|
||||
DataInputStream dataInputStream = new DataInputStream(inputStream);
|
||||
dataInputStream.readFully(bytes);
|
||||
return bytes;
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to download latest release: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}).orElse(null);
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to locate latest release: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareVersions(String version1, String version2) {
|
||||
String[] v1Parts = version1.split("[-.]");
|
||||
String[] v2Parts = version2.split("[-.]");
|
||||
|
||||
int length = Math.min(v1Parts.length, v2Parts.length);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int comparison = compareVersionPart(v1Parts[i], v2Parts[i]);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're here, all compared parts are equal
|
||||
if (v1Parts.length != v2Parts.length) {
|
||||
return compareSpecialVersions(v1Parts, v2Parts);
|
||||
}
|
||||
|
||||
return 0; // Versions are equal
|
||||
}
|
||||
|
||||
private int compareVersionPart(String part1, String part2) {
|
||||
try {
|
||||
int v1 = Integer.parseInt(part1);
|
||||
int v2 = Integer.parseInt(part2);
|
||||
return Integer.compare(v1, v2);
|
||||
} catch (NumberFormatException e) {
|
||||
// If parts are not numbers, compare them based on dev < snapshot < release
|
||||
return compareSpecialPart(part1, part2);
|
||||
}
|
||||
}
|
||||
|
||||
private int compareSpecialPart(String part1, String part2) {
|
||||
if (part1.equals(part2)) return 0;
|
||||
if (part1.startsWith("dev")) return part2.startsWith("dev") ? 0 : -1;
|
||||
if (part2.startsWith("dev")) return 1;
|
||||
if (part1.equals("SNAPSHOT")) return part2.equals("SNAPSHOT") ? 0 : -1;
|
||||
if (part2.equals("SNAPSHOT")) return 1;
|
||||
return part1.compareTo(part2);
|
||||
}
|
||||
|
||||
private int compareSpecialVersions(String[] v1Parts, String[] v2Parts) {
|
||||
if (v1Parts.length > v2Parts.length) {
|
||||
String specialPart = v1Parts[v2Parts.length];
|
||||
if (specialPart.startsWith("dev")) return -1;
|
||||
if (specialPart.equals("SNAPSHOT")) return -1;
|
||||
return 1; // Assume it's a release version part
|
||||
} else {
|
||||
String specialPart = v2Parts[v1Parts.length];
|
||||
if (specialPart.startsWith("dev")) return 1;
|
||||
if (specialPart.equals("SNAPSHOT")) return 1;
|
||||
return -1; // Assume it's a release version part
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void saveDefaultConfig();
|
||||
|
||||
public abstract PermissionChecker getPermissionChecker();
|
||||
|
||||
public abstract float getTickRate();
|
||||
|
||||
public abstract URL getJarURL();
|
||||
|
||||
public abstract void restartServer();
|
||||
|
||||
public abstract boolean hasRestartScript();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package me.caseload.knockbacksync;
|
||||
|
||||
import me.caseload.knockbacksync.util.NumberConversions;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConfigWrapper {
|
||||
private final Map<String, Object> configMap;
|
||||
|
||||
public ConfigWrapper(Map<String, Object> configMap) {
|
||||
this.configMap = configMap;
|
||||
}
|
||||
|
||||
public String getString(String path, String def) {
|
||||
Object value = getValue(path);
|
||||
return value instanceof String ? (String) value : def;
|
||||
}
|
||||
|
||||
public boolean getBoolean(String path, boolean def) {
|
||||
Object value = getValue(path);
|
||||
return value instanceof Boolean ? (Boolean) value : def;
|
||||
}
|
||||
|
||||
public int getInt(String path, int def) {
|
||||
Object value = getValue(path);
|
||||
return NumberConversions.toInt(value, def);
|
||||
}
|
||||
|
||||
public long getLong(String path, long def) {
|
||||
Object value = getValue(path);
|
||||
return NumberConversions.toLong(value, def);
|
||||
}
|
||||
|
||||
public double getDouble(String path, double def) {
|
||||
Object value = getValue(path);
|
||||
if (value instanceof Number) return ((Number) value).doubleValue();
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
return Double.parseDouble((String) value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
public void set(String path, Object value) {
|
||||
setValue(path, value);
|
||||
}
|
||||
|
||||
private Object getValue(String path) {
|
||||
String[] parts = path.split("\\.");
|
||||
Object current = configMap;
|
||||
for (String part : parts) {
|
||||
if (current instanceof Map) {
|
||||
current = ((Map<?, ?>) current).get(part);
|
||||
} else {
|
||||
return null; // Path not found
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private void setValue(String path, Object value) {
|
||||
String[] parts = path.split("\\.");
|
||||
Map<String, Object> current = configMap;
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
String part = parts[i];
|
||||
if (!current.containsKey(part) || !(current.get(part) instanceof Map)) {
|
||||
current.put(part, new HashMap<>());
|
||||
}
|
||||
current = (Map<String, Object>) current.get(part);
|
||||
}
|
||||
current.put(parts[parts.length - 1], value);
|
||||
}
|
||||
|
||||
public boolean contains(String path) {
|
||||
return getValue(path) != null;
|
||||
}
|
||||
|
||||
public Object get(String path) {
|
||||
return getValue(path);
|
||||
}
|
||||
|
||||
public Set<String> getKeys(String path) {
|
||||
Object value = getValue(path);
|
||||
if (value instanceof Map) {
|
||||
return ((Map<String, Object>) value).keySet();
|
||||
}
|
||||
return new HashSet<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.caseload.knockbacksync;
|
||||
|
||||
public enum Platform {
|
||||
FABRIC,
|
||||
BUKKIT,
|
||||
FOLIA
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package me.caseload.knockbacksync.command;
|
||||
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
|
||||
public class MainCommand implements BuilderCommand {
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.handler(context -> {
|
||||
context.sender().sendMessage(
|
||||
ChatUtil.translateAlternateColorCodes(
|
||||
'&',
|
||||
"&6This server is running the &eKnockbackSync &6plugin. &bhttps://github.com/CASELOAD7000/knockback-sync"
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package me.caseload.knockbacksync.command.generic;
|
||||
|
||||
import org.incendo.cloud.context.CommandContext;
|
||||
import org.incendo.cloud.parser.ParserDescriptor;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public abstract class AbstractPlayerSelectorParser<C> {
|
||||
|
||||
public abstract ParserDescriptor<C, PlayerSelector> descriptor();
|
||||
|
||||
protected abstract ParserDescriptor<C, ?> getPlatformSpecificDescriptor();
|
||||
|
||||
protected abstract CompletableFuture<PlayerSelector> adaptToCommonSelector(CommandContext<C> context, Object platformSpecificSelector);
|
||||
|
||||
// Helper method to create the ParserDescriptor
|
||||
protected ParserDescriptor<C, PlayerSelector> createDescriptor() {
|
||||
return ParserDescriptor.of(
|
||||
getPlatformSpecificDescriptor().parser().mapSuccess(this::adaptToCommonSelector),
|
||||
PlayerSelector.class
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.caseload.knockbacksync.command.generic;
|
||||
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
|
||||
public interface BuilderCommand {
|
||||
|
||||
void register(CommandManager<Sender> manager);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package me.caseload.knockbacksync.command.generic;
|
||||
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface PlayerSelector {
|
||||
boolean isSingle();
|
||||
|
||||
PlatformPlayer getSinglePlayer(); // Throws an exception if not a single selection
|
||||
|
||||
Collection<PlatformPlayer> getPlayers();
|
||||
|
||||
String inputString();
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package me.caseload.knockbacksync.command.subcommand;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.ConfigWrapper;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.command.generic.PlayerSelector;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.permission.PredicatePermission;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class PingCommand implements BuilderCommand {
|
||||
|
||||
private static final ConfigManager configManager = Base.INSTANCE.getConfigManager();
|
||||
|
||||
private String pingSelfAvailableMessage;
|
||||
private String pingSelfUnavailableMessage;
|
||||
private String pingOtherAvailableMessage;
|
||||
private String pingOtherUnavailableMessage;
|
||||
|
||||
private String mustSpecifyPlayerFromConsoleMessage;
|
||||
private String knockbacksyncDisabledForYouMessage;
|
||||
private String knockbacksyncDisabledForTargetMessage;
|
||||
|
||||
public PingCommand() {
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.permission((sender -> {
|
||||
final String permission = "knockbacksync.ping";
|
||||
Predicate<Sender> senderPredicate = (s) -> {
|
||||
return s.hasPermission(permission, true);
|
||||
};
|
||||
|
||||
return PredicatePermission.of(senderPredicate).testPermission(sender);
|
||||
}))
|
||||
.literal("ping")
|
||||
.optional("target", Base.INSTANCE.getPlayerSelectorParser().descriptor())
|
||||
.handler(context -> {
|
||||
PlayerSelector targetSelector = context.getOrDefault("target", null);
|
||||
|
||||
if (targetSelector == null) {
|
||||
if (context.sender().isConsole()) {
|
||||
context.sender().sendMessage(ChatUtil.translateAlternateColorCodes('&', mustSpecifyPlayerFromConsoleMessage));
|
||||
} else {
|
||||
context.sender().sendMessage(getPingMessage(context.sender().getUniqueId(), context.sender().getName(), null, null));
|
||||
}
|
||||
} else {
|
||||
context.sender().sendMessage(getPingMessage(context.sender().getUniqueId(), context.sender().getName(), targetSelector.getSinglePlayer().getUUID(), targetSelector.getSinglePlayer().getName()));
|
||||
}
|
||||
})
|
||||
);
|
||||
Base.INSTANCE.getEventBus().registerListeners(this);
|
||||
}
|
||||
private void loadConfig() {
|
||||
ConfigWrapper configWrapper = configManager.getConfigWrapper();
|
||||
pingSelfAvailableMessage = configWrapper.getString("messages.ping.self.available",
|
||||
"Your real ping is &b%ping%&rms. Jitter: &b%jitter%&rms. Spike: &b%spike%&r. Compensated ping: &b%compensated%&rms.");
|
||||
pingSelfUnavailableMessage = configWrapper.getString("messages.ping.self.unavailable",
|
||||
"Accurate ping unavailable. Your estimated ping is &b%ping%&rms.");
|
||||
pingOtherAvailableMessage = configWrapper.getString("messages.ping.other.available",
|
||||
"%player%'s real ping is &b%ping%&rms. Jitter: &b%jitter%&rms. Spike: &b%spike%&r. Compensated ping: &b%compensated%&rms.");
|
||||
pingOtherUnavailableMessage = configWrapper.getString("messages.ping.other.unavailable",
|
||||
"Accurate ping unavailable. %player%'s estimated ping is &b%ping%&rms.");
|
||||
mustSpecifyPlayerFromConsoleMessage = configWrapper.getString("messages.console.must_specify_player",
|
||||
"&cYou must specify a player to use the knockbacksync ping command from the console.");
|
||||
knockbacksyncDisabledForYouMessage = configWrapper.getString("messages.disabled.self",
|
||||
"&cKnockback synchronization is currently disabled for you!");
|
||||
knockbacksyncDisabledForTargetMessage = configWrapper.getString("messages.disabled.target",
|
||||
"&cKnockback synchronization is currently disabled for the target player.");
|
||||
}
|
||||
|
||||
private String getPingMessage(UUID senderUUID, String senderName, UUID targetUUID, String targetName) {
|
||||
boolean isSelf = senderUUID.equals(targetUUID) || targetUUID == null;
|
||||
String message;
|
||||
if (isSelf) {
|
||||
message = knockbacksyncDisabledForYouMessage;
|
||||
targetUUID = senderUUID;
|
||||
targetName = senderName;
|
||||
} else {
|
||||
message = knockbacksyncDisabledForTargetMessage;
|
||||
}
|
||||
|
||||
User targetUser = Base.INSTANCE.getPlatformServer().getPlayer(targetUUID).getUser();
|
||||
if (targetUser == null)
|
||||
return ChatUtil.translateAlternateColorCodes('&',
|
||||
configManager.getPlayerDisconnectedWhileExecutingCommand()
|
||||
).replace("%player%", targetName);
|
||||
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(targetUser);
|
||||
if (playerData == null)
|
||||
return ChatUtil.translateAlternateColorCodes('&', message);
|
||||
|
||||
String rawReturnString;
|
||||
if (playerData.getPing() == null) {
|
||||
rawReturnString = isSelf ? pingSelfUnavailableMessage : pingOtherUnavailableMessage;
|
||||
} else {
|
||||
rawReturnString = isSelf ? pingSelfAvailableMessage : pingOtherAvailableMessage;
|
||||
}
|
||||
|
||||
rawReturnString = rawReturnString
|
||||
.replace("%player%", playerData.getPlatformPlayer().getName())
|
||||
.replace("%ping%", playerData.getPing() == null ?
|
||||
String.valueOf(playerData.getPlatformPlayer().getPing()) :
|
||||
String.format("%.3f", playerData.getPing()))
|
||||
.replace("%jitter%", String.format("%.3f", playerData.getJitter()))
|
||||
.replace("%spike%", String.valueOf(playerData.isSpike()))
|
||||
.replace("%compensated%", String.format("%.3f", playerData.getCompensatedPing()));
|
||||
|
||||
return ChatUtil.translateAlternateColorCodes('&', rawReturnString);
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReload(ConfigReloadEvent event) {
|
||||
loadConfig();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package me.caseload.knockbacksync.command.subcommand;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.permission.PredicatePermission;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class ReloadCommand implements BuilderCommand {
|
||||
|
||||
private static final ConfigManager configManager = Base.INSTANCE.getConfigManager();
|
||||
private String rawReloadMessage = configManager.getConfigWrapper().getString("messages.reload.success", "&aSuccessfully reloaded KnockbackSync.");
|
||||
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.literal("reload")
|
||||
.permission((sender -> {
|
||||
final String permission = "knockbacksync.reload";
|
||||
Predicate<Sender> senderPredicate = (s) -> {
|
||||
return s.hasPermission(permission, false);
|
||||
};
|
||||
|
||||
return PredicatePermission.of(senderPredicate).testPermission(sender);
|
||||
}))
|
||||
.handler(context -> {
|
||||
configManager.loadConfig(true);
|
||||
|
||||
// Fire the ConfigReloadEvent
|
||||
new ConfigReloadEvent(configManager).post();
|
||||
|
||||
String reloadMessage = ChatUtil.translateAlternateColorCodes('&', rawReloadMessage);
|
||||
|
||||
context.sender().sendMessage(reloadMessage);
|
||||
})
|
||||
);
|
||||
Base.INSTANCE.getEventBus().registerListeners(this);
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReload(ConfigReloadEvent event) {
|
||||
rawReloadMessage = event.getConfigManager().getConfigWrapper().getString("messages.reload.success", "&aSuccessfully reloaded KnockbackSync.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package me.caseload.knockbacksync.command.subcommand;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.ConfigWrapper;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.command.generic.PlayerSelector;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.permission.PredicatePermission;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class StatusCommand implements BuilderCommand {
|
||||
|
||||
private static final ConfigManager configManager = Base.INSTANCE.getConfigManager();
|
||||
private static final String STATUS_SELF_PERMISSION = "knockbacksync.status.self";
|
||||
private static final String STATUS_OTHER_PERMISSION = "knockbacksync.status.other";
|
||||
|
||||
private String globalStatusEnabledMessage;
|
||||
private String globalStatusDisabledMessage;
|
||||
private String globalOffGroundStatusEnabledMessage;
|
||||
private String globalOffGroundStatusDisabledMessage;
|
||||
private String playerStatusEnabledMessage;
|
||||
private String playerStatusDisabledMessage;
|
||||
private String playerStatusGlobalDisabledMessage;
|
||||
private String noSelfPermissionMessage;
|
||||
private String noOtherPermissionMessage;
|
||||
|
||||
public StatusCommand() {
|
||||
updateConfigValues();
|
||||
}
|
||||
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.literal("status")
|
||||
.optional("target", Base.INSTANCE.getPlayerSelectorParser().descriptor())
|
||||
.permission((sender -> {
|
||||
Predicate<Sender> senderPredicate = (s) -> {
|
||||
return s.hasPermission(STATUS_SELF_PERMISSION, true) || sender.hasPermission(STATUS_OTHER_PERMISSION, false);
|
||||
};
|
||||
|
||||
return PredicatePermission.of(senderPredicate).testPermission(sender);
|
||||
}))
|
||||
.handler(context -> {
|
||||
Sender sender = context.sender();
|
||||
PlayerSelector targetSelector = context.getOrDefault("target", null);
|
||||
if (targetSelector == null) {
|
||||
// Show global status
|
||||
boolean globalStatus = configManager.isToggled();
|
||||
sender.sendMessage(
|
||||
ChatUtil.translateAlternateColorCodes('&',
|
||||
(globalStatus ? globalStatusEnabledMessage : globalStatusDisabledMessage))
|
||||
+ "\n"
|
||||
+ ChatUtil.translateAlternateColorCodes('&',
|
||||
ToggleOffGroundSubcommand.offGroundSyncEnabled ? globalOffGroundStatusEnabledMessage : globalOffGroundStatusDisabledMessage)
|
||||
);
|
||||
|
||||
if (sender.hasPermission(STATUS_SELF_PERMISSION, true)) {
|
||||
// Show player status for the sender (no target specified)
|
||||
if (!sender.isConsole()) {
|
||||
showPlayerStatus(sender, Base.INSTANCE.getPlatformServer().getPlayer(sender.getUniqueId()));
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noSelfPermissionMessage));
|
||||
}
|
||||
} else {
|
||||
if (sender.hasPermission(STATUS_OTHER_PERMISSION, true)) {
|
||||
PlatformPlayer target = targetSelector.getSinglePlayer();
|
||||
showPlayerStatus(sender, target);
|
||||
} else {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noOtherPermissionMessage));
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
Base.INSTANCE.getEventBus().registerListeners(this);
|
||||
}
|
||||
|
||||
private void showPlayerStatus(Sender sender, PlatformPlayer target) {
|
||||
if (target.getUser() == null) {
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
configManager.getPlayerDisconnectedWhileExecutingCommand()
|
||||
).replace("%player%", target.getName());
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
|
||||
boolean globalStatus = configManager.isToggled();
|
||||
boolean playerStatus = PlayerDataManager.containsPlayerData(target.getUser());
|
||||
|
||||
String statusMessage;
|
||||
if (!globalStatus) {
|
||||
statusMessage = playerStatusGlobalDisabledMessage.replace("%player%", target.getName());
|
||||
} else {
|
||||
statusMessage = (playerStatus ? this.playerStatusEnabledMessage : this.playerStatusDisabledMessage).replace("%player%", target.getName());
|
||||
}
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', statusMessage));
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReload(ConfigReloadEvent event) {
|
||||
updateConfigValues();
|
||||
}
|
||||
|
||||
private void updateConfigValues() {
|
||||
ConfigWrapper config = configManager.getConfigWrapper();
|
||||
this.globalStatusEnabledMessage = config.getString("messages.status.global.enabled",
|
||||
"&eKnockbackSync global status: &aEnabled");
|
||||
this.globalStatusDisabledMessage = config.getString("messages.status.global.disabled",
|
||||
"&eKnockbackSync global status: &cDisabled");
|
||||
this.globalOffGroundStatusEnabledMessage = config.getString("messages.status.offground.enabled",
|
||||
"&eKnockbackSync off-ground status: &aEnabled");
|
||||
this.globalOffGroundStatusDisabledMessage = config.getString("messages.status.offground.disabled",
|
||||
"&eKnockbackSync off-ground status: &cDisabled");
|
||||
this.playerStatusEnabledMessage = config.getString("messages.status.player.enabled",
|
||||
"&e%player%'s KnockbackSync status: &aEnabled");
|
||||
this.playerStatusDisabledMessage = config.getString("messages.status.player.disabled",
|
||||
"&e%player%'s KnockbackSync status: &cDisabled");
|
||||
this.playerStatusGlobalDisabledMessage = config.getString("messages.status.player.global_disabled",
|
||||
"&e%player%'s KnockbackSync status: &cDisabled (Global toggle is off)");
|
||||
this.noSelfPermissionMessage = config.getString("messages.status.permission.no_self",
|
||||
"&cYou do not have permission to check your knockbacksync status.");
|
||||
this.noOtherPermissionMessage = config.getString("messages.status.permission.no_other",
|
||||
"&cYou do not have permission to check the status of other players!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package me.caseload.knockbacksync.command.subcommand;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.ConfigWrapper;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.command.generic.PlayerSelector;
|
||||
import me.caseload.knockbacksync.event.events.ToggleOnOffEvent;
|
||||
import me.caseload.knockbacksync.manager.CombatManager;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.permission.PermissionChecker;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.permission.PredicatePermission;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class ToggleCommand implements BuilderCommand {
|
||||
|
||||
private static final ConfigManager configManager = Base.INSTANCE.getConfigManager();
|
||||
private static final PermissionChecker permissionChecker = Base.INSTANCE.getPermissionChecker();
|
||||
private static final String TOGGLE_GLOBAL_PERMISSION = "knockbacksync.toggle.global";
|
||||
private static final String TOGGLE_SELF_PERMISSION = "knockbacksync.toggle.self";
|
||||
private static final String TOGGLE_OTHER_PERMISSION = "knockbacksync.toggle.other";
|
||||
|
||||
private String noGlobalPermissionMessage;
|
||||
private String noSelfPermissionMessage;
|
||||
private String noOtherPermissionMessage;
|
||||
private String serverDisabledMessage;
|
||||
|
||||
public ToggleCommand() {
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
ConfigWrapper configWrapper = configManager.getConfigWrapper();
|
||||
noGlobalPermissionMessage = configWrapper.getString("messages.toggle.permission.no_global",
|
||||
"&cYou don't have permission to toggle the global setting.");
|
||||
noSelfPermissionMessage = configWrapper.getString("messages.toggle.permission.no_self",
|
||||
"&cYou do not have permission to toggle your knockback.");
|
||||
noOtherPermissionMessage = configWrapper.getString("messages.toggle.permission.no_other",
|
||||
"&cYou do not have permission to toggle the knockback of other player's.");
|
||||
serverDisabledMessage = configWrapper.getString("messages.toggle.server_disabled",
|
||||
"&cKnockbacksync is currently disabled on this server. Contact your server administrator for more information.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.literal("toggle")
|
||||
.optional("target", Base.INSTANCE.getPlayerSelectorParser().descriptor())
|
||||
.permission((sender -> {
|
||||
Predicate<Sender> senderPredicate = (s) -> {
|
||||
return s.hasPermission(TOGGLE_GLOBAL_PERMISSION, false)
|
||||
|| sender.hasPermission(TOGGLE_SELF_PERMISSION, false)
|
||||
|| sender.hasPermission(TOGGLE_OTHER_PERMISSION, false);
|
||||
};
|
||||
|
||||
return PredicatePermission.of(senderPredicate).testPermission(sender);
|
||||
}))
|
||||
.handler(context -> {
|
||||
Sender sender = context.sender();
|
||||
PlayerSelector targetSelector = context.getOrDefault("target", null);
|
||||
if (targetSelector == null) {
|
||||
// Global toggle
|
||||
if (permissionChecker.hasPermission(sender, TOGGLE_GLOBAL_PERMISSION, false)) {
|
||||
toggleGlobalKnockback(sender);
|
||||
} else {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noGlobalPermissionMessage));
|
||||
}
|
||||
} else {
|
||||
PlatformPlayer target = targetSelector.getSinglePlayer();
|
||||
boolean senderIsTarget = sender.getUniqueId() == target.getUUID();
|
||||
if (!senderIsTarget && !permissionChecker.hasPermission(sender, TOGGLE_OTHER_PERMISSION, false)) {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noOtherPermissionMessage));
|
||||
return;
|
||||
} else if (senderIsTarget && !permissionChecker.hasPermission(sender, TOGGLE_SELF_PERMISSION, true)) {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noSelfPermissionMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configManager.isToggled()) {
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', serverDisabledMessage));
|
||||
} else {
|
||||
togglePlayerKnockback(target, sender);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private static void toggleGlobalKnockback(Sender sender) {
|
||||
boolean toggledState = !configManager.isToggled();
|
||||
ToggleOnOffEvent toggleOnOffEvent = new ToggleOnOffEvent(toggledState);
|
||||
toggleOnOffEvent.post();
|
||||
if (toggleOnOffEvent.isCancelled())
|
||||
return;
|
||||
|
||||
toggledState = toggleOnOffEvent.getStatus();
|
||||
configManager.setToggled(toggledState);
|
||||
|
||||
configManager.getConfigWrapper().set("enabled", toggledState);
|
||||
configManager.saveConfig();
|
||||
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
toggledState ? configManager.getEnableMessage() : configManager.getDisableMessage()
|
||||
);
|
||||
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', message));
|
||||
}
|
||||
|
||||
private static void togglePlayerKnockback(PlatformPlayer target, Sender sender) {
|
||||
User user = target.getUser();
|
||||
if (user == null) {
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
configManager.getPlayerDisconnectedWhileExecutingCommand()
|
||||
).replace("%player%", target.getName());
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
|
||||
if (PlayerDataManager.shouldExempt(target.getUUID())) {
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
configManager.getPlayerIneligibleMessage()
|
||||
).replace("%player%", target.getName());
|
||||
|
||||
sender.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasPlayerData = PlayerDataManager.containsPlayerData(user);
|
||||
if (hasPlayerData) {
|
||||
if (CombatManager.getPlayers().contains(user)) {
|
||||
CombatManager.removePlayer(user);
|
||||
}
|
||||
PlayerDataManager.removePlayerData(user);
|
||||
} else {
|
||||
PlayerDataManager.addPlayerData(user, target);
|
||||
}
|
||||
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
hasPlayerData ? configManager.getPlayerDisableMessage() : configManager.
|
||||
getPlayerEnableMessage()
|
||||
).replace("%player%", target.getName());
|
||||
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package me.caseload.knockbacksync.command.subcommand;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.ConfigWrapper;
|
||||
import me.caseload.knockbacksync.command.generic.BuilderCommand;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.permission.PredicatePermission;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class ToggleOffGroundSubcommand implements BuilderCommand {
|
||||
|
||||
public static boolean offGroundSyncEnabled = true;
|
||||
private String offGroundSyncEnableMessage;
|
||||
private String offGroundSyncDisableMessage;
|
||||
|
||||
public ToggleOffGroundSubcommand() {
|
||||
loadConfigSettings();
|
||||
}
|
||||
|
||||
public void register(CommandManager<Sender> manager) {
|
||||
manager.command(
|
||||
manager.commandBuilder("knockbacksync", "kbsync", "kbs")
|
||||
.literal("toggleoffground")
|
||||
.permission((sender -> {
|
||||
final String permission = "knockbacksync.toggleoffground";
|
||||
Predicate<Sender> senderPredicate = (s) -> {
|
||||
return s.hasPermission(permission, false);
|
||||
};
|
||||
|
||||
return PredicatePermission.of(senderPredicate).testPermission(sender);
|
||||
}))
|
||||
.handler(commandContext -> {
|
||||
offGroundSyncEnabled = !offGroundSyncEnabled;
|
||||
Base.INSTANCE.getConfigManager().getConfigWrapper().set("enable_offground_synchronization", offGroundSyncEnabled);
|
||||
Base.INSTANCE.getConfigManager().saveConfig();
|
||||
String message = ChatUtil.translateAlternateColorCodes('&',
|
||||
offGroundSyncEnabled ? offGroundSyncEnableMessage : offGroundSyncDisableMessage);
|
||||
commandContext.sender().sendMessage(message);
|
||||
}));
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReloadEvent(ConfigReloadEvent event) {
|
||||
loadConfigSettings();
|
||||
}
|
||||
|
||||
private void loadConfigSettings() {
|
||||
ConfigWrapper configWrapper = Base.INSTANCE.getConfigManager().getConfigWrapper();
|
||||
offGroundSyncEnabled = configWrapper.getBoolean("enable_offground_synchronization", true);
|
||||
this.offGroundSyncEnableMessage = configWrapper.getString("messages.offground.enable",
|
||||
"&aSuccessfully enabled offground synchronization.");
|
||||
this.offGroundSyncDisableMessage = configWrapper.getString("messages.offground.disable",
|
||||
"&cSuccessfully disabled offground synchronization.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package me.caseload.knockbacksync.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
|
||||
public class YamlConfiguration {
|
||||
private final File file;
|
||||
private final Yaml yaml;
|
||||
@Getter
|
||||
@Setter
|
||||
private Map<String, Object> data;
|
||||
private String fileContents;
|
||||
|
||||
public YamlConfiguration(File file) {
|
||||
this.file = file;
|
||||
DumperOptions options = new DumperOptions();
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
options.setPrettyFlow(true);
|
||||
options.setWidth(Integer.MAX_VALUE);
|
||||
this.yaml = new Yaml(options);
|
||||
}
|
||||
|
||||
public void load() throws IOException {
|
||||
if (!file.exists()) {
|
||||
data = new LinkedHashMap<>();
|
||||
return;
|
||||
}
|
||||
|
||||
fileContents = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
|
||||
data = yaml.load(fileContents);
|
||||
if (data == null) {
|
||||
data = new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
if (!file.exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
}
|
||||
|
||||
String dump = yaml.dump(data);
|
||||
|
||||
if (fileContents != null) {
|
||||
Map<String, YamlElement> oldElements = parseYaml(fileContents);
|
||||
Map<String, YamlElement> newElements = parseYaml(dump);
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
buildOutput(oldElements, newElements, result);
|
||||
dump = result.toString();
|
||||
}
|
||||
|
||||
try (Writer writer = new OutputStreamWriter(Files.newOutputStream(file.toPath()), StandardCharsets.UTF_8)) {
|
||||
writer.write(dump);
|
||||
}
|
||||
}
|
||||
|
||||
private static class YamlElement {
|
||||
String key;
|
||||
String value;
|
||||
int indent;
|
||||
boolean isComment;
|
||||
List<YamlElement> children = new ArrayList<>();
|
||||
|
||||
YamlElement(String line, int indent) {
|
||||
this.indent = indent;
|
||||
if (line.trim().startsWith("#")) {
|
||||
this.isComment = true;
|
||||
this.value = line;
|
||||
} else {
|
||||
this.isComment = false;
|
||||
String[] parts = line.split(":", 2);
|
||||
this.key = parts[0].trim();
|
||||
this.value = parts.length > 1 ? parts[1].trim() : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, YamlElement> parseYaml(String content) {
|
||||
Map<String, YamlElement> elements = new LinkedHashMap<>();
|
||||
Stack<YamlElement> stack = new Stack<>();
|
||||
String[] lines = content.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
|
||||
int indent = getIndentation(line);
|
||||
YamlElement element = new YamlElement(line, indent);
|
||||
|
||||
while (!stack.isEmpty() && stack.peek().indent >= indent) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
if (!element.isComment) {
|
||||
elements.put(element.key, element);
|
||||
} else {
|
||||
elements.put("comment" + elements.size(), element);
|
||||
}
|
||||
} else {
|
||||
stack.peek().children.add(element);
|
||||
}
|
||||
|
||||
if (!element.isComment && element.value.isEmpty()) {
|
||||
stack.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
private void buildOutput(Map<String, YamlElement> oldElements, Map<String, YamlElement> newElements, StringBuilder result) {
|
||||
for (YamlElement element : oldElements.values()) {
|
||||
if (element.isComment) {
|
||||
result.append(element.value).append("\n");
|
||||
} else {
|
||||
YamlElement newElement = newElements.get(element.key);
|
||||
if (newElement != null) {
|
||||
String indent = createIndent(element.indent);
|
||||
result.append(indent).append(element.key).append(":");
|
||||
|
||||
if (!newElement.value.isEmpty()) {
|
||||
result.append(" ").append(newElement.value);
|
||||
}
|
||||
result.append("\n");
|
||||
|
||||
if (!element.children.isEmpty()) {
|
||||
Map<String, YamlElement> oldChildren = new LinkedHashMap<>();
|
||||
Map<String, YamlElement> newChildren = new LinkedHashMap<>();
|
||||
|
||||
for (YamlElement child : element.children) {
|
||||
if (!child.isComment) {
|
||||
oldChildren.put(child.key, child);
|
||||
} else {
|
||||
oldChildren.put("comment" + oldChildren.size(), child);
|
||||
}
|
||||
}
|
||||
|
||||
for (YamlElement child : newElement.children) {
|
||||
if (!child.isComment) {
|
||||
newChildren.put(child.key, child);
|
||||
}
|
||||
}
|
||||
|
||||
buildOutput(oldChildren, newChildren, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getIndentation(String line) {
|
||||
int indent = 0;
|
||||
while (indent < line.length() && line.charAt(indent) == ' ') {
|
||||
indent++;
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
|
||||
private String createIndent(int count) {
|
||||
StringBuilder sb = new StringBuilder(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package me.caseload.knockbacksync.event;
|
||||
|
||||
import lombok.Setter;
|
||||
import me.caseload.knockbacksync.event.OptimizedEventBus;
|
||||
|
||||
public abstract class Event {
|
||||
@Setter
|
||||
private static EventBus eventBus;
|
||||
@Setter
|
||||
private boolean cancelled = false;
|
||||
|
||||
public void post() {
|
||||
if (eventBus != null) {
|
||||
eventBus.post(this);
|
||||
} else {
|
||||
throw new IllegalStateException("EventBus has not been set");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.caseload.knockbacksync.event;
|
||||
|
||||
public interface EventBus {
|
||||
void registerListeners(Object listener);
|
||||
|
||||
void registerStaticListeners(Class<?> clazz);
|
||||
|
||||
void unregisterListeners(Object listener);
|
||||
|
||||
void unregisterStaticListeners(Class<?> clazz);
|
||||
|
||||
void post(Event event);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package me.caseload.knockbacksync.event;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface KBSyncEventHandler {
|
||||
int priority() default 0;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package me.caseload.knockbacksync.event;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class OptimizedEventBus implements EventBus {
|
||||
// ordinarily this would just be a hashmap since we don't modify it on different threads, but Folia forces us to make it concurrent
|
||||
private final Map<Class<? extends Event>, List<OptimizedListener>> listenerMap = new ConcurrentHashMap<>();
|
||||
private final MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
|
||||
@Override
|
||||
public void registerListeners(Object listener) {
|
||||
registerMethods(listener, listener.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStaticListeners(Class<?> clazz) {
|
||||
registerMethods(null, clazz);
|
||||
}
|
||||
|
||||
private void registerMethods(Object instance, Class<?> clazz) {
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
KBSyncEventHandler annotation = method.getAnnotation(KBSyncEventHandler.class);
|
||||
if (annotation != null && method.getParameterCount() == 1) {
|
||||
Class<?> eventType = method.getParameterTypes()[0];
|
||||
if (Event.class.isAssignableFrom(eventType)) {
|
||||
try {
|
||||
// Skip instance methods when registering static listeners
|
||||
if (instance == null && !Modifier.isStatic(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MethodHandle handle = lookup.unreflect(method);
|
||||
OptimizedListener optimizedListener = new OptimizedListener(instance, handle, annotation.priority(), method.getDeclaringClass());
|
||||
// Yes the CopyOnWriteArrayList is slow, yes we need to for thread safety on Folia, we'll replace when performance problems actually crop up
|
||||
listenerMap.computeIfAbsent((Class<? extends Event>) eventType, k -> new CopyOnWriteArrayList<>()).add(optimizedListener);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort listeners by priority
|
||||
listenerMap.values().forEach(list -> list.sort((a, b) -> Integer.compare(b.priority, a.priority)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListeners(Object listener) {
|
||||
listenerMap.values().forEach(list -> list.removeIf(l -> l.instance == listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterStaticListeners(Class<?> clazz) {
|
||||
listenerMap.values().forEach(list -> list.removeIf(l -> l.instance == null && l.declaringClass == clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(Event event) {
|
||||
List<OptimizedListener> listeners = listenerMap.get(event.getClass());
|
||||
if (listeners != null) {
|
||||
for (OptimizedListener listener : listeners) {
|
||||
try {
|
||||
if (listener.instance != null) {
|
||||
// Instance method
|
||||
listener.handle.invoke(listener.instance, event);
|
||||
} else {
|
||||
// Static method
|
||||
listener.handle.invoke(event);
|
||||
}
|
||||
if (event.isCancelled()) {
|
||||
break;
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class OptimizedListener {
|
||||
final Object instance; // null for static methods
|
||||
final MethodHandle handle;
|
||||
final int priority;
|
||||
final Class<?> declaringClass;
|
||||
|
||||
OptimizedListener(Object instance, MethodHandle handle, int priority, Class<?> declaringClass) {
|
||||
this.instance = instance;
|
||||
this.handle = handle;
|
||||
this.priority = priority;
|
||||
this.declaringClass = declaringClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package me.caseload.knockbacksync.event.events;
|
||||
|
||||
import lombok.Getter;
|
||||
import me.caseload.knockbacksync.event.Event;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
|
||||
@Getter
|
||||
public class ConfigReloadEvent extends Event {
|
||||
private final ConfigManager configManager;
|
||||
|
||||
public ConfigReloadEvent(ConfigManager configManager) {
|
||||
this.configManager = configManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package me.caseload.knockbacksync.event.events;
|
||||
|
||||
import me.caseload.knockbacksync.event.Event;
|
||||
|
||||
public class ToggleOnOffEvent extends Event {
|
||||
|
||||
private boolean newEnabledState;
|
||||
|
||||
public ToggleOnOffEvent(boolean newEnabledState) {
|
||||
this.newEnabledState = newEnabledState;
|
||||
}
|
||||
|
||||
public boolean getStatus() {
|
||||
return newEnabledState;
|
||||
}
|
||||
|
||||
public void setStatus(boolean newEnabledState) {
|
||||
this.newEnabledState = newEnabledState;
|
||||
}
|
||||
@Override
|
||||
public void setCancelled(boolean cancelled) {
|
||||
super.setCancelled(cancelled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package me.caseload.knockbacksync.listener;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.LegacyKnockback;
|
||||
|
||||
public abstract class PlayerDamageListener {
|
||||
public void onPlayerDamage(PlatformPlayer victim, PlatformPlayer attacker) {
|
||||
ConfigManager config = Base.INSTANCE.getConfigManager();
|
||||
if (!config.isToggled())
|
||||
return;
|
||||
|
||||
User user = victim.getUser();
|
||||
if (user == null) return; // Prevent errors with players disconnecting while this is running (or with fake player?)
|
||||
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData == null)
|
||||
return;
|
||||
|
||||
if (config.isLegacyKnockbackEnabled()) {
|
||||
if (playerData.isRodKnockbackPending()) {
|
||||
// A fishing-rod (bobber-direction) knockback was already supplied for this hit via
|
||||
// the rod API. Keep it instead of overwriting with the melee formula; the velocity
|
||||
// event still owns + latency-syncs it. Its vertical was set by the API call.
|
||||
} else {
|
||||
// KnockbackSync owns the full 1.8 melee velocity. Compute it now (we have the
|
||||
// attacker here) and store it for the PlayerVelocityEvent to apply + sync.
|
||||
Vector3d legacy = LegacyKnockback.compute(victim, attacker, playerData, config);
|
||||
playerData.setLegacyKnockback(legacy);
|
||||
// On-ground sync should target the legacy vertical value, not the 1.9 one.
|
||||
playerData.setVerticalVelocity(legacy.getY());
|
||||
}
|
||||
} else {
|
||||
playerData.setVerticalVelocity(playerData.calculateVerticalVelocity(attacker)); // do not move this calculation
|
||||
}
|
||||
playerData.setLastDamageTicks(victim.getNoDamageTicks());
|
||||
playerData.updateCombat();
|
||||
|
||||
if (!Base.INSTANCE.getConfigManager().isRunnableEnabled())
|
||||
playerData.sendPing(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package me.caseload.knockbacksync.listener;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
|
||||
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public abstract class PlayerKnockbackListener {
|
||||
|
||||
/**
|
||||
* Computes the melee velocity to apply to the victim.
|
||||
*
|
||||
* <p>The returned value, when non-null, MUST be written back by the platform layer.
|
||||
* Bukkit writes it to the {@code PlayerVelocityEvent} (so KnockbackSync is the last,
|
||||
* authoritative writer in the event chain rather than reading/writing the player's
|
||||
* live velocity out-of-band — see issue #7).</p>
|
||||
*
|
||||
* <p>When legacy (1.8) knockback is enabled, the 1.8 vector computed at damage time
|
||||
* replaces vanilla X/Z/Y, and latency Y-correction is applied on top of it. In that
|
||||
* mode this method always returns a velocity (the raw 1.8 vector when no correction
|
||||
* applies), so KnockbackSync owns the final melee velocity.</p>
|
||||
*
|
||||
* @return the velocity to apply, or {@code null} to leave the velocity unchanged.
|
||||
*/
|
||||
@Nullable
|
||||
public Vector3d onPlayerVelocity(PlatformPlayer victim, Vector3d velocity) {
|
||||
ConfigManager config = Base.INSTANCE.getConfigManager();
|
||||
if (!config.isToggled())
|
||||
return null;
|
||||
|
||||
User user = victim.getUser();
|
||||
if (user == null) return null; // Prevent errors with players disconnecting while this is running (or with fake player?)
|
||||
|
||||
PlayerData victimPlayerData = PlayerDataManager.getPlayerData(user);
|
||||
if (victimPlayerData == null)
|
||||
return null;
|
||||
|
||||
// Legacy 1.8 knockback: KnockbackSync owns the melee velocity. The 1.8 vector
|
||||
// (computed at damage time) replaces vanilla X/Z/Y. We still apply latency
|
||||
// Y-correction on top when conditions allow; otherwise the raw 1.8 vector is
|
||||
// applied. legacyKb == null means either legacy is off or none is pending, in
|
||||
// which case behaviour falls back to the original (Y-sync of the event velocity).
|
||||
Vector3d legacyKb = config.isLegacyKnockbackEnabled() ? victimPlayerData.consumeLegacyKnockback() : null;
|
||||
Vector3d base = (legacyKb != null) ? legacyKb : velocity;
|
||||
|
||||
if (victimPlayerData.getNotNullPing() < PlayerData.PING_OFFSET)
|
||||
return legacyKb;
|
||||
|
||||
double distanceToGround = victimPlayerData.getDistanceToGround();
|
||||
if (distanceToGround <= 0)
|
||||
return legacyKb; // minecraft already does the work for us
|
||||
|
||||
WrappedBlockState blockState = victim.getWorld().getBlockStateAt(victim.getLocation());
|
||||
if (victim.isGliding() ||
|
||||
blockState.getType() == StateTypes.WATER ||
|
||||
blockState.getType() == StateTypes.LAVA ||
|
||||
blockState.getType() == StateTypes.COBWEB ||
|
||||
blockState.getType() == StateTypes.SCAFFOLDING)
|
||||
return legacyKb;
|
||||
|
||||
Vector3d adjustedVelocity;
|
||||
if (victimPlayerData.isOnGroundClientSide(base.getY(), distanceToGround)) {
|
||||
Integer damageTicks = victimPlayerData.getLastDamageTicks();
|
||||
if (damageTicks != null && damageTicks > 8)
|
||||
return legacyKb;
|
||||
|
||||
adjustedVelocity = base.withY(victimPlayerData.getVerticalVelocity()); // Should be impossible to produce a NPE in this context
|
||||
}
|
||||
else if (victimPlayerData.isOffGroundSyncEnabled())
|
||||
adjustedVelocity = base.withY(victimPlayerData.getCompensatedOffGroundVelocity());
|
||||
else
|
||||
return legacyKb;
|
||||
|
||||
return adjustedVelocity;
|
||||
}
|
||||
}
|
||||
+29
-23
@@ -1,15 +1,16 @@
|
||||
package me.caseload.knockbacksync.listener;
|
||||
package me.caseload.knockbacksync.listener.packetevents;
|
||||
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketSendEvent;
|
||||
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes;
|
||||
import me.caseload.knockbacksync.manager.PlayerData;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.MathUtil;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -19,9 +20,9 @@ public class AttributeChangeListener extends PacketListenerAbstract {
|
||||
public static final UUID SPRINTING_MODIFIER_UUID =
|
||||
UUID.fromString("662A6B8D-DA3E-4C1C-8813-96EA6097278D");
|
||||
|
||||
double minGravity = -1;
|
||||
double maxGravity = 1;
|
||||
double defaultGravity = 0.08;
|
||||
final double minGravity = -1;
|
||||
final double maxGravity = 1;
|
||||
final double defaultGravity = 0.08;
|
||||
double currentGravity;
|
||||
|
||||
public AttributeChangeListener() {
|
||||
@@ -30,23 +31,23 @@ public class AttributeChangeListener extends PacketListenerAbstract {
|
||||
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
// Intercept the packet that updates entity properties (attributes)
|
||||
if (event.getPacketType() != PacketType.Play.Server.UPDATE_ATTRIBUTES)
|
||||
return;
|
||||
if (!Base.INSTANCE.getConfigManager().isToggled()) return;
|
||||
|
||||
WrapperPlayServerUpdateAttributes packet = new WrapperPlayServerUpdateAttributes(event);
|
||||
if (event.getPacketType() == PacketType.Play.Server.UPDATE_ATTRIBUTES) {
|
||||
|
||||
// Check if the entity is a player
|
||||
Player player = event.getPlayer();
|
||||
if (player == null || !PlayerDataManager.containsPlayerData(player.getUniqueId()))
|
||||
return;
|
||||
WrapperPlayServerUpdateAttributes packet = new WrapperPlayServerUpdateAttributes(event);
|
||||
|
||||
// Get the attributes from the packet
|
||||
for (WrapperPlayServerUpdateAttributes.Property property : packet.getProperties()) {
|
||||
// You can now check for specific attributes
|
||||
if (property.getAttribute().equals(Attributes.GENERIC_GRAVITY)) {
|
||||
onPlayerGravityChange(player, calculateValueWithModifiers(property));
|
||||
break;
|
||||
User user = event.getUser();
|
||||
if (!PlayerDataManager.containsPlayerData(user)) return;
|
||||
|
||||
// Get the attributes from the packet
|
||||
for (WrapperPlayServerUpdateAttributes.Property property : packet.getProperties()) {
|
||||
// You can now check for specific attributes
|
||||
if (property.getAttribute().equals(Attributes.GENERIC_GRAVITY)) {
|
||||
onPlayerGravityChange(user, calculateValueWithModifiers(property));
|
||||
} else if (property.getAttribute().equals(Attributes.GENERIC_KNOCKBACK_RESISTANCE)) {
|
||||
onPlayerKnockBackChange(user, calculateValueWithModifiers(property));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,12 +87,17 @@ public class AttributeChangeListener extends PacketListenerAbstract {
|
||||
|
||||
// Yes this is not properly latency compensated, that would require including a proper simulation engine
|
||||
// Laggy players will just have to deal with being on the wrong gravity for a few hundred ms, too bad!
|
||||
public void onPlayerGravityChange(Player player, double newGravity) {
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(player.getUniqueId());
|
||||
public void onPlayerGravityChange(User user, double newGravity) {
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_5)) {
|
||||
playerData.setGravity(newGravity);
|
||||
playerData.setGravityAttribute(newGravity);
|
||||
} else {
|
||||
currentGravity = defaultGravity;
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlayerKnockBackChange(User user, double newKnockbackResistance) {
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
playerData.setKnockbackResistanceAttribute(newKnockbackResistance);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package me.caseload.knockbacksync.listener.packetevents;
|
||||
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPluginMessage;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
|
||||
public class ClientBrandListener extends PacketListenerAbstract {
|
||||
String brand = "vanilla";
|
||||
boolean hasBrand = false;
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(final PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLUGIN_MESSAGE) {
|
||||
WrapperPlayClientPluginMessage packet = new WrapperPlayClientPluginMessage(event);
|
||||
String channelName = packet.getChannelName();
|
||||
handle(channelName, packet.getData());
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser());
|
||||
if (playerData == null) return;
|
||||
playerData.getPlatformPlayer().setClientBrand(brand);
|
||||
}
|
||||
}
|
||||
|
||||
public void handle(String channel, byte[] data) {
|
||||
if (channel.equalsIgnoreCase("minecraft:brand") || // 1.13+
|
||||
channel.equals("MC|Brand")) { // 1.12
|
||||
if (data.length > 64 || data.length == 0) {
|
||||
brand = "sent " + data.length + " bytes as brand";
|
||||
} else if (!hasBrand) {
|
||||
byte[] minusLength = new byte[data.length - 1];
|
||||
System.arraycopy(data, 1, minusLength, 0, minusLength.length);
|
||||
brand = new String(minusLength).replace(" (Velocity)", ""); //removes velocity's brand suffix
|
||||
}
|
||||
hasBrand = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package me.caseload.knockbacksync.listener.packetevents;
|
||||
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.UserDisconnectEvent;
|
||||
import com.github.retrooper.packetevents.event.UserLoginEvent;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.google.common.base.Preconditions;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.ChatUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PacketPlayerJoinQuit extends PacketListenerAbstract {
|
||||
@Override
|
||||
public void onUserLogin(UserLoginEvent event) {
|
||||
Object nativePlayerObject = event.getPlayer();
|
||||
Preconditions.checkArgument(nativePlayerObject != null);
|
||||
|
||||
@NotNull PlatformPlayer platformPlayer = Base.INSTANCE.getPlatformServer().getPlayer(nativePlayerObject);
|
||||
onPlayerJoin(event.getUser(), platformPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserDisconnect(UserDisconnectEvent event) {
|
||||
onPlayerQuit(event.getUser());
|
||||
}
|
||||
|
||||
public void onPlayerJoin(User user, PlatformPlayer player) {
|
||||
PlayerDataManager.addPlayerData(user, player);
|
||||
|
||||
if (Base.INSTANCE.getConfigManager().isUpdateAvailable() && Base.INSTANCE.getConfigManager().isNotifyUpdate() && Base.INSTANCE.getPermissionChecker().hasPermission(player, "knockbacksync.update"))
|
||||
player.sendMessage(ChatUtil.translateAlternateColorCodes(
|
||||
'&',
|
||||
"&6An updated version of &eKnockbackSync &6is now available for download at: &bhttps://github.com/CASELOAD7000/knockback-sync/releases/latest"
|
||||
));
|
||||
}
|
||||
|
||||
public void onPlayerQuit(@NotNull User user) {
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData == null)
|
||||
return;
|
||||
|
||||
playerData.quitCombat(true);
|
||||
PlayerDataManager.removePlayerData(user);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package me.caseload.knockbacksync.listener.packetevents;
|
||||
|
||||
import com.github.retrooper.packetevents.event.*;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientKeepAlive;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPong;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientWindowConfirmation;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PingStrategy;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.data.Pair;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PingReceiveListener extends PacketListenerAbstract {
|
||||
|
||||
public PingReceiveListener() {
|
||||
super(PacketListenerPriority.LOWEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
// Do not immediately return if KB sync is disabled. This is because if we send a packet, disable the plugin
|
||||
// And then receive a response we have to ensure that if the packet was sent by us we cancel it
|
||||
// otherwise the server will likely kick the player
|
||||
PacketTypeCommon packetType = event.getPacketType();
|
||||
User user = event.getUser();
|
||||
if (user == null) return;
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData == null) return;
|
||||
|
||||
// Cancel echoes of injected packets before any strategy check: on 1.20.2+ an unexpected keepalive is an instant "Timed out" kick, so a strategy switch mid-flight must not leak one.
|
||||
if (packetType == PacketType.Play.Client.KEEP_ALIVE) {
|
||||
WrapperPlayClientKeepAlive keepAlive = new WrapperPlayClientKeepAlive(event);
|
||||
long receivedId = keepAlive.getId();
|
||||
|
||||
if (playerData.didWeSendThatPacket(receivedId))
|
||||
event.setCancelled(true);
|
||||
|
||||
if (playerData.pingStrategy == PingStrategy.KEEPALIVE)
|
||||
handlePingCalculationPackets(playerData, receivedId, playerData.keepaliveMap);
|
||||
} else if (packetType == PacketType.Play.Client.PONG) {
|
||||
WrapperPlayClientPong pong = new WrapperPlayClientPong(event);
|
||||
int id = pong.getId();
|
||||
|
||||
if (playerData.didWeSendThatPacket(id))
|
||||
event.setCancelled(true);
|
||||
|
||||
if (playerData.pingStrategy == PingStrategy.TRANSACTION)
|
||||
handlePingCalculationPackets(playerData, id, playerData.transactionsSent);
|
||||
} else if (packetType == PacketType.Play.Client.WINDOW_CONFIRMATION) {
|
||||
WrapperPlayClientWindowConfirmation windowConfirmation = new WrapperPlayClientWindowConfirmation(event);
|
||||
int id = windowConfirmation.getActionId();
|
||||
|
||||
if (playerData.didWeSendThatPacket(id))
|
||||
event.setCancelled(true);
|
||||
|
||||
if (playerData.pingStrategy == PingStrategy.TRANSACTION)
|
||||
handlePingCalculationPackets(playerData, id, playerData.transactionsSent);
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Number> void handlePingCalculationPackets(PlayerData playerData, long id, Queue<Pair<T, Long>> packetSentList) {
|
||||
// System.out.println("Received response ID: " + id + " Queue size before: " + packetSentList.size());
|
||||
// System.out.println("Current queue contents: " + packetSentList.toString());
|
||||
|
||||
if (!Base.INSTANCE.getConfigManager().isToggled()) return;
|
||||
|
||||
Pair<T, Long> data = null;
|
||||
int cleared = 0;
|
||||
// Keep polling until we find the matching ID
|
||||
do {
|
||||
data = packetSentList.poll();
|
||||
cleared++;
|
||||
|
||||
if (data == null) {
|
||||
// System.out.println("No data found in queue!");
|
||||
break;
|
||||
}
|
||||
|
||||
// System.out.println("Cleared entry " + cleared + ": ID=" + data.getFirst() + " Time=" + data.getSecond());
|
||||
|
||||
long pingNanos = (System.nanoTime() - data.getSecond());
|
||||
double diffMillisDouble = pingNanos / 1_000_000.0;
|
||||
|
||||
playerData.setPreviousPing(playerData.getPing());
|
||||
playerData.setPing(diffMillisDouble);
|
||||
|
||||
playerData.getJitterCalculator().addPing(pingNanos);
|
||||
double jitter = playerData.getJitterCalculator().calculateJitter();
|
||||
playerData.setJitter(jitter);
|
||||
|
||||
} while (data.getFirst().longValue() != id);
|
||||
|
||||
// System.out.println("Finished processing - Cleared " + cleared + " entries. Queue size after: " + packetSentList.size());
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package me.caseload.knockbacksync.listener.packetevents;
|
||||
|
||||
import com.github.retrooper.packetevents.event.*;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientKeepAlive;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPong;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientWindowConfirmation;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerKeepAlive;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPing;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowConfirmation;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PingStrategy;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.data.Pair;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PingSendListener extends PacketListenerAbstract {
|
||||
|
||||
public PingSendListener() {
|
||||
// See all actually outgoing, not cancelled, ping packets
|
||||
super(PacketListenerPriority.MONITOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
if (!Base.INSTANCE.getConfigManager().isToggled()) return;
|
||||
if (event.isCancelled()) return;
|
||||
|
||||
PacketTypeCommon packetType = event.getPacketType();
|
||||
UUID playerUUID = event.getUser().getUUID();
|
||||
if (playerUUID == null) return;
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser());
|
||||
if (playerData == null) return;
|
||||
|
||||
if (playerData.pingStrategy == PingStrategy.KEEPALIVE && packetType.equals(PacketType.Play.Server.KEEP_ALIVE)) {
|
||||
WrapperPlayServerKeepAlive keepAlive = new WrapperPlayServerKeepAlive(event);
|
||||
long id = keepAlive.getId();
|
||||
|
||||
// System.out.println("Adding ping to queue - ID: " + id + " Time: " + System.nanoTime() + " Queue size before: " + playerData.keepaliveMap.size());
|
||||
playerData.keepaliveMap.add(new Pair<>(id, System.nanoTime()));
|
||||
} else if (playerData.pingStrategy == PingStrategy.TRANSACTION && packetType.equals(PacketType.Play.Server.PING)) {
|
||||
WrapperPlayServerPing ping = new WrapperPlayServerPing(event);
|
||||
int id = ping.getId();
|
||||
|
||||
playerData.transactionsSent.add(new Pair<>(id, System.nanoTime()));
|
||||
} else if (playerData.pingStrategy == PingStrategy.TRANSACTION && packetType.equals(PacketType.Play.Server.WINDOW_CONFIRMATION)) {
|
||||
WrapperPlayServerWindowConfirmation confirmation = new WrapperPlayServerWindowConfirmation(event);
|
||||
int id = confirmation.getActionId();
|
||||
|
||||
playerData.transactionsSent.add(new Pair<>(id, System.nanoTime()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package me.caseload.knockbacksync.manager;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class CombatManager {
|
||||
|
||||
private static final Set<@NotNull User> combatPlayers = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public static @NotNull Set<@NotNull User> getPlayers() {
|
||||
return combatPlayers;
|
||||
}
|
||||
|
||||
public static void addPlayer(@NotNull User user) {
|
||||
combatPlayers.add(user);
|
||||
}
|
||||
|
||||
public static void removePlayer(@NotNull User user) {
|
||||
combatPlayers.remove(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package me.caseload.knockbacksync.manager;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import me.caseload.knockbacksync.ConfigWrapper;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.Platform;
|
||||
import me.caseload.knockbacksync.config.YamlConfiguration;
|
||||
import me.caseload.knockbacksync.runnable.PingRunnable;
|
||||
import me.caseload.knockbacksync.scheduler.AbstractTaskHandle;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class ConfigManager {
|
||||
|
||||
public static final long CONFIG_VERSION = 9;
|
||||
|
||||
private boolean toggled;
|
||||
private boolean runnableEnabled;
|
||||
private boolean updateAvailable;
|
||||
private boolean notifyUpdate;
|
||||
private boolean autoUpdate;
|
||||
|
||||
// Legacy (1.8) knockback: makes KnockbackSync the single owner of melee velocity,
|
||||
// reproducing OldCombatMechanics' old-player-knockback values while still latency-syncing.
|
||||
private boolean legacyKnockbackEnabled;
|
||||
private double legacyKnockbackHorizontal;
|
||||
private double legacyKnockbackVertical;
|
||||
private double legacyKnockbackVerticalLimit;
|
||||
private double legacyKnockbackExtraHorizontal;
|
||||
private double legacyKnockbackExtraVertical;
|
||||
private boolean legacyKnockbackResistanceEnabled;
|
||||
|
||||
private long runnableInterval;
|
||||
private long combatTimer;
|
||||
private long spikeThreshold;
|
||||
|
||||
private String enableMessage;
|
||||
private String disableMessage;
|
||||
private String playerEnableMessage;
|
||||
private String playerDisableMessage;
|
||||
private String playerIneligibleMessage;
|
||||
private String playerDisconnectedWhileExecutingCommand;
|
||||
|
||||
private AbstractTaskHandle pingTask;
|
||||
|
||||
private Map<String, Object> config;
|
||||
private File configFile;
|
||||
private ConfigWrapper configWrapper; // Cache the ConfigWrapper instance
|
||||
private YamlConfiguration yamlConfig;
|
||||
|
||||
public ConfigManager() {
|
||||
Base instance = Base.INSTANCE;
|
||||
configFile = new File(instance.getDataFolder(), "config.yml");
|
||||
yamlConfig = new YamlConfiguration(configFile);
|
||||
}
|
||||
|
||||
public ConfigWrapper getConfigWrapper() {
|
||||
if (configWrapper == null) {
|
||||
reloadConfig();
|
||||
}
|
||||
return configWrapper;
|
||||
}
|
||||
|
||||
public void reloadConfig() {
|
||||
try {
|
||||
if (!configFile.exists()) {
|
||||
Base.INSTANCE.saveDefaultConfig();
|
||||
}
|
||||
yamlConfig.load();
|
||||
config = yamlConfig.getData();
|
||||
configWrapper = new ConfigWrapper(config);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
config = new HashMap<>();
|
||||
configWrapper = new ConfigWrapper(config);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveConfig() {
|
||||
try {
|
||||
yamlConfig.setData(config);
|
||||
yamlConfig.save();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadConfig(boolean reloadConfig) {
|
||||
if (reloadConfig || config == null) {
|
||||
reloadConfig();
|
||||
}
|
||||
|
||||
ConfigWrapper configWrapper = getConfigWrapper(); // Use cached ConfigWrapper
|
||||
|
||||
updateConfig();
|
||||
|
||||
toggled = configWrapper.getBoolean("enabled", true);
|
||||
|
||||
boolean newRunnableEnabled = configWrapper.getBoolean("runnable.enabled", true);
|
||||
// Always stop the previous ping task on reload (reschedule below only if still enabled); gating the cancel on staying enabled leaked the task when disabling via reload.
|
||||
if (pingTask != null) { // null on first startup
|
||||
pingTask.cancel();
|
||||
pingTask = null;
|
||||
}
|
||||
|
||||
runnableEnabled = newRunnableEnabled;
|
||||
runnableInterval = configWrapper.getLong("runnable.interval", 5L);
|
||||
|
||||
if (runnableEnabled) {
|
||||
long initialDelay = 0L;
|
||||
long pingTaskRunnableInterval = runnableInterval;
|
||||
// Folia does not allow 0 ticks of wait time
|
||||
if (Base.INSTANCE.getPlatform() == Platform.FOLIA) {
|
||||
initialDelay = 1L;
|
||||
pingTaskRunnableInterval = Math.max(pingTaskRunnableInterval, 1L);
|
||||
}
|
||||
pingTask = Base.INSTANCE.getScheduler().runTaskTimerAsynchronously(new PingRunnable(), initialDelay, pingTaskRunnableInterval);
|
||||
}
|
||||
|
||||
legacyKnockbackEnabled = configWrapper.getBoolean("legacy_knockback.enabled", false);
|
||||
legacyKnockbackHorizontal = configWrapper.getDouble("legacy_knockback.horizontal", 0.4);
|
||||
legacyKnockbackVertical = configWrapper.getDouble("legacy_knockback.vertical", 0.4);
|
||||
legacyKnockbackVerticalLimit = configWrapper.getDouble("legacy_knockback.vertical_limit", 0.4);
|
||||
legacyKnockbackExtraHorizontal = configWrapper.getDouble("legacy_knockback.extra_horizontal", 0.5);
|
||||
legacyKnockbackExtraVertical = configWrapper.getDouble("legacy_knockback.extra_vertical", 0.1);
|
||||
legacyKnockbackResistanceEnabled = configWrapper.getBoolean("legacy_knockback.enable_knockback_resistance", false);
|
||||
|
||||
notifyUpdate = configWrapper.getBoolean("notify_updates", true);
|
||||
autoUpdate = configWrapper.getBoolean("auto_update", true);
|
||||
combatTimer = configWrapper.getLong("runnable.combat_timer", 30L);
|
||||
spikeThreshold = configWrapper.getLong("spike_threshold", 20L);
|
||||
enableMessage = configWrapper.getString("messages.toggle.global.enable", "&aSuccessfully enabled KnockbackSync.");
|
||||
disableMessage = configWrapper.getString("messages.toggle.global.disable", "&cSuccessfully disabled KnockbackSync.");
|
||||
playerEnableMessage = configWrapper.getString("messages.toggle.player.enable", "&aSuccessfully enabled KnockbackSync for %player%.");
|
||||
playerDisableMessage = configWrapper.getString("messages.toggle.player.disable", "&aSuccessfully &cdisabled &aKnockbackSync for %player%.");
|
||||
playerIneligibleMessage = configWrapper.getString("messages.toggle.player.ineligible", "&c%player% is ineligible for KnockbackSync. If you believe this is in error, please contact your server administrators.");
|
||||
playerDisconnectedWhileExecutingCommand = configWrapper.getString("messages.toggle.player.disconnected-while-executing-command", "&c%player% disconnected while executing command.");
|
||||
}
|
||||
|
||||
public void updateConfig() {
|
||||
ConfigWrapper oldConfig = getConfigWrapper();
|
||||
long oldConfigVersion = oldConfig.getLong("config_version", 0);
|
||||
|
||||
if (oldConfigVersion < CONFIG_VERSION) {
|
||||
// Backup old config with comments
|
||||
File backupFile = new File(configFile.getParentFile(), "config-version-" + oldConfigVersion + ".yml");
|
||||
try {
|
||||
Files.move(configFile.toPath(), backupFile.toPath());
|
||||
Base.INSTANCE.getLogger().info("Backed up old config to " + backupFile.getName());
|
||||
} catch (IOException e) {
|
||||
Base.INSTANCE.getLogger().warning("Failed to backup old config: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Store old values
|
||||
Map<String, Object> oldValues = new HashMap<>(config);
|
||||
|
||||
// Create new config with default values
|
||||
Base.INSTANCE.saveDefaultConfig();
|
||||
reloadConfig();
|
||||
|
||||
// Transfer existing settings
|
||||
ConfigWrapper newConfig = getConfigWrapper();
|
||||
transferAllSettings(oldConfig, newConfig);
|
||||
|
||||
// Set new config version
|
||||
newConfig.set("config_version", CONFIG_VERSION);
|
||||
|
||||
// Save updated config
|
||||
saveConfig();
|
||||
|
||||
Base.INSTANCE.getLogger().info("Config updated to version " + CONFIG_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
private void transferAllSettings(ConfigWrapper oldConfig, ConfigWrapper newConfig) {
|
||||
transferSettingsRecursive(oldConfig, newConfig, ".");
|
||||
}
|
||||
|
||||
private void transferSettingsRecursive(ConfigWrapper oldConfig, ConfigWrapper newConfig, String currentPath) {
|
||||
for (String key : oldConfig.getKeys(currentPath)) {
|
||||
String fullPath = currentPath.isEmpty() || currentPath.equals(".") ? key : currentPath + "." + key;
|
||||
if (newConfig.contains(fullPath)) {
|
||||
Object value = oldConfig.get(fullPath);
|
||||
if (value instanceof Map) {
|
||||
transferSettingsRecursive(oldConfig, newConfig, fullPath);
|
||||
} else {
|
||||
newConfig.set(fullPath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package me.caseload.knockbacksync.manager;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.util.FloodgateUtil;
|
||||
import me.caseload.knockbacksync.util.GeyserUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PlayerDataManager {
|
||||
|
||||
private static final Map<User, PlayerData> playerDataMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static @Nullable PlayerData getPlayerData(@NotNull User user) {
|
||||
return playerDataMap.get(user);
|
||||
}
|
||||
|
||||
public static void addPlayerData(@NotNull User user, @NotNull PlatformPlayer platformPlayer) {
|
||||
if (!shouldExempt(platformPlayer.getUUID())) {
|
||||
PlayerData playerData = new PlayerData(user, platformPlayer);
|
||||
playerDataMap.put(user, playerData);
|
||||
Base.INSTANCE.getEventBus().registerListeners(playerData);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removePlayerData(@NotNull User user) {
|
||||
PlayerData playerData = playerDataMap.remove(user);
|
||||
if (playerData != null)
|
||||
Base.INSTANCE.getEventBus().unregisterListeners(playerData);
|
||||
}
|
||||
|
||||
public static boolean containsPlayerData(@NotNull User user) {
|
||||
return playerDataMap.containsKey(user);
|
||||
}
|
||||
|
||||
public static boolean shouldExempt(@NotNull UUID uuid) {
|
||||
// Geyser players don't have Java movement
|
||||
return GeyserUtil.isGeyserPlayer(uuid)
|
||||
// Floodgate is the authentication system for Geyser on servers that use Geyser as a proxy instead of installing it as a plugin directly on the server
|
||||
|| FloodgateUtil.isFloodgatePlayer(uuid)
|
||||
// Geyser formatted player string
|
||||
// This will never happen for Java players, as the first character in the 3rd group is always 4 (xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx)
|
||||
|| uuid.toString().startsWith("00000000-0000-0000-0009");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package me.caseload.knockbacksync.permission;
|
||||
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.sender.Sender;
|
||||
|
||||
public interface PermissionChecker {
|
||||
boolean hasPermission(Object nativeType, String s, boolean defaultIfUnset);
|
||||
|
||||
boolean hasPermission(Sender source, String s, boolean defaultIfUnset);
|
||||
|
||||
boolean hasPermission(PlatformPlayer platform, String s);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package me.caseload.knockbacksync.player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JitterCalculator {
|
||||
private final int SAMPLE_SIZE = 15;
|
||||
private final Queue<Long> pings = new LinkedList<>();
|
||||
|
||||
public void addPing(long pingTime) {
|
||||
pings.offer(pingTime);
|
||||
if (pings.size() > SAMPLE_SIZE) {
|
||||
pings.poll();
|
||||
}
|
||||
}
|
||||
|
||||
public double calculateJitter() {
|
||||
if (pings.size() < 2) return 0;
|
||||
|
||||
List<Long> sortedPings = new ArrayList<>(pings);
|
||||
Collections.sort(sortedPings);
|
||||
|
||||
// Calculate IQR
|
||||
int q1Index = sortedPings.size() / 4;
|
||||
int q3Index = q1Index * 3;
|
||||
long q1 = sortedPings.get(q1Index);
|
||||
long q3 = sortedPings.get(q3Index);
|
||||
long iqr = q3 - q1;
|
||||
|
||||
// Filter outliers
|
||||
double lowerBound = q1 - 1.5 * iqr;
|
||||
double upperBound = q3 + 1.5 * iqr;
|
||||
List<Long> filteredPings = sortedPings.stream()
|
||||
.filter(p -> p >= lowerBound && p <= upperBound)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Calculate standard deviation
|
||||
double mean = filteredPings.stream().mapToLong(Long::longValue).average().orElse(0);
|
||||
double variance = filteredPings.stream()
|
||||
.mapToDouble(p -> Math.pow(p - mean, 2))
|
||||
.average().orElse(0);
|
||||
double stdDev = Math.sqrt(variance);
|
||||
|
||||
// Calculate mean jitter
|
||||
double meanJitter = 0;
|
||||
Long prevPing = null;
|
||||
for (Long ping : filteredPings) {
|
||||
if (prevPing != null) {
|
||||
meanJitter += Math.abs(ping - prevPing);
|
||||
}
|
||||
prevPing = ping;
|
||||
}
|
||||
meanJitter /= (filteredPings.size() - 1);
|
||||
|
||||
// You can return different jitter metrics based on your needs
|
||||
return stdDev / 1_000_000.0; // or meanJitter, or both
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package me.caseload.knockbacksync.player;
|
||||
|
||||
public enum PingStrategy {
|
||||
KEEPALIVE,
|
||||
TRANSACTION
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package me.caseload.knockbacksync.player;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.protocol.world.BoundingBox;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.world.PlatformWorld;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PlatformPlayer {
|
||||
UUID getUUID();
|
||||
|
||||
String getName();
|
||||
|
||||
double getX();
|
||||
|
||||
double getY();
|
||||
|
||||
double getZ();
|
||||
|
||||
float getPitch();
|
||||
|
||||
float getYaw();
|
||||
|
||||
boolean isOnGround();
|
||||
|
||||
int getPing();
|
||||
|
||||
boolean isGliding();
|
||||
|
||||
PlatformWorld getWorld();
|
||||
|
||||
Vector3d getLocation();
|
||||
|
||||
void sendMessage(@NotNull String s);
|
||||
|
||||
double getAttackCooldown();
|
||||
|
||||
boolean isSprinting();
|
||||
|
||||
int getMainHandKnockbackLevel();
|
||||
|
||||
/**
|
||||
* Knockback enchant level used for legacy (1.8) knockback bonus calculation.
|
||||
* Mirrors OldCombatMechanics: the main hand item, or the off hand item when the
|
||||
* main hand is empty. Defaults to the main hand level for platforms that do not
|
||||
* override it.
|
||||
*/
|
||||
default int getWeaponKnockbackLevel() {
|
||||
return getMainHandKnockbackLevel();
|
||||
}
|
||||
|
||||
@Nullable Integer getNoDamageTicks();
|
||||
|
||||
void setVelocity(Vector3d adjustedVelocity);
|
||||
|
||||
Vector3d getVelocity();
|
||||
|
||||
double getJumpPower();
|
||||
|
||||
BoundingBox getBoundingBox();
|
||||
|
||||
/**
|
||||
* If a player disconnects while we are running the constructor of a PlatformPlayer
|
||||
* PacketEvents.getAPI().getPlayerManager().getUser(bukkitPlayer) may return null
|
||||
* This will make the internal field null and thus this function may return null.
|
||||
*/
|
||||
@Nullable User getUser();
|
||||
|
||||
void setClientBrand(String brand);
|
||||
|
||||
String getClientBrand();
|
||||
// Add more methods as needed
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
package me.caseload.knockbacksync.player;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.manager.player.PlayerManager;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.netty.channel.ChannelHelper;
|
||||
import com.github.retrooper.packetevents.protocol.ConnectionState;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import com.github.retrooper.packetevents.protocol.world.BoundingBox;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerKeepAlive;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPing;
|
||||
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowConfirmation;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.command.subcommand.ToggleOffGroundSubcommand;
|
||||
import me.caseload.knockbacksync.event.KBSyncEventHandler;
|
||||
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
|
||||
import me.caseload.knockbacksync.event.events.ToggleOnOffEvent;
|
||||
import me.caseload.knockbacksync.manager.CombatManager;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.scheduler.AbstractTaskHandle;
|
||||
import me.caseload.knockbacksync.scheduler.NettyTaskHandle;
|
||||
import me.caseload.knockbacksync.util.MathUtil;
|
||||
import me.caseload.knockbacksync.util.data.Pair;
|
||||
import me.caseload.knockbacksync.world.PlatformWorld;
|
||||
import me.caseload.knockbacksync.world.raytrace.FluidHandling;
|
||||
import me.caseload.knockbacksync.world.raytrace.RayTraceResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import io.netty.channel.Channel;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public class PlayerData {
|
||||
|
||||
// Please read the GitHub FAQ before adjusting.
|
||||
private static final short MAIN_THREAD_TRANSACTION_ID = 31407;
|
||||
private static final short NETTY_THREAD_TRANSACTION_ID = 31408;
|
||||
public static final long PING_OFFSET = 25;
|
||||
|
||||
private static Field playerField;
|
||||
|
||||
public final Queue<Pair<Integer, Long>> transactionsSent = new ConcurrentLinkedQueue<>();
|
||||
public final Queue<Pair<Long, Long>> keepaliveMap = new ConcurrentLinkedQueue<>();
|
||||
|
||||
static {
|
||||
try {
|
||||
switch (Base.INSTANCE.getPlatform()) {
|
||||
case BUKKIT:
|
||||
case FOLIA:
|
||||
Class<?> bukkitPlayerClass = Class.forName("me.caseload.knockbacksync.player.BukkitPlayer");
|
||||
playerField = bukkitPlayerClass.getDeclaredField("bukkitPlayer");
|
||||
break;
|
||||
case FABRIC:
|
||||
Class<?> fabricPlayerClass = Class.forName("me.caseload.knockbacksync.player.FabricPlayer");
|
||||
playerField = fabricPlayerClass.getDeclaredField("fabricPlayer");
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected platform: " + Base.INSTANCE.getPlatform());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
playerField.setAccessible(true); // May not be needed since it's already public
|
||||
}
|
||||
|
||||
public final User user;
|
||||
private final PlatformPlayer platformPlayer;
|
||||
private final UUID uuid;
|
||||
@NotNull private final Random random = new Random();
|
||||
public long lastKeepAliveID = 0;
|
||||
@Getter private final JitterCalculator jitterCalculator = new JitterCalculator();
|
||||
@Setter private double jitter;
|
||||
@Nullable private AbstractTaskHandle combatTask;
|
||||
@NotNull private final Object combatTaskLock = new Object(); // Lock object for synchronization
|
||||
@Nullable @Setter private Double ping, previousPing;
|
||||
@Nullable @Setter private Double verticalVelocity;
|
||||
@Nullable @Setter private Integer lastDamageTicks;
|
||||
// Legacy (1.8) knockback computed at damage time, consumed by the next PlayerVelocityEvent.
|
||||
// Guarded by a short expiry so a stale value never affects an unrelated velocity event.
|
||||
@Nullable private Vector3d legacyKnockback;
|
||||
private long legacyKnockbackExpiryNanos;
|
||||
// True when the pending legacyKnockback is a fishing-rod (bobber-direction) vector supplied
|
||||
// via the rod API rather than the melee formula. Lets the damage listener keep the rod vector
|
||||
// instead of overwriting it with melee, while still latency-syncing it. See api.KnockbackSyncRodApi.
|
||||
private boolean legacyKnockbackIsRod;
|
||||
@Setter private double gravityAttribute = 0.08;
|
||||
@Setter private double knockbackResistanceAttribute = 0.0;
|
||||
public PingStrategy pingStrategy; // this is currently shared between all instances, but can be made per-player later
|
||||
|
||||
public PlayerData(User user, PlatformPlayer platformPlayer) {
|
||||
this.uuid = platformPlayer.getUUID();
|
||||
this.user = user;
|
||||
this.platformPlayer = platformPlayer;
|
||||
this.pingStrategy = loadPingStrategy(Base.INSTANCE.getConfigManager());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the legacy (1.8) knockback to apply on the next PlayerVelocityEvent.
|
||||
* Expires shortly after so it can never be applied to an unrelated velocity event
|
||||
* (explosions, other plugins, etc.) if the expected velocity event never fires.
|
||||
*/
|
||||
public void setLegacyKnockback(@Nullable Vector3d legacyKnockback) {
|
||||
this.legacyKnockback = legacyKnockback;
|
||||
this.legacyKnockbackIsRod = false;
|
||||
// 100ms window: damage and the resulting velocity event fire in the same tick.
|
||||
this.legacyKnockbackExpiryNanos = System.nanoTime() + 100_000_000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a fishing-rod (bobber-direction) knockback to apply + latency-sync on the next
|
||||
* PlayerVelocityEvent, flagged so the damage listener keeps it instead of overwriting it with
|
||||
* the melee formula. Must be set immediately before the rod's damage tick. See
|
||||
* {@code api.KnockbackSyncRodApi}.
|
||||
*/
|
||||
public void setRodKnockback(@NotNull Vector3d rodKnockback) {
|
||||
this.legacyKnockback = rodKnockback;
|
||||
this.legacyKnockbackIsRod = true;
|
||||
this.legacyKnockbackExpiryNanos = System.nanoTime() + 100_000_000L;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if an unexpired fishing-rod knockback is pending (so the damage listener must
|
||||
* not overwrite it with melee knockback).
|
||||
*/
|
||||
public boolean isRodKnockbackPending() {
|
||||
return peekLegacyKnockback() != null && legacyKnockbackIsRod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the pending legacy knockback if present and not expired, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Vector3d peekLegacyKnockback() {
|
||||
if (legacyKnockback == null) return null;
|
||||
if (System.nanoTime() > legacyKnockbackExpiryNanos) {
|
||||
legacyKnockback = null;
|
||||
legacyKnockbackIsRod = false;
|
||||
return null;
|
||||
}
|
||||
return legacyKnockback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns and clears the pending legacy knockback (if present and not expired).
|
||||
*/
|
||||
@Nullable
|
||||
public Vector3d consumeLegacyKnockback() {
|
||||
Vector3d v = peekLegacyKnockback();
|
||||
legacyKnockback = null;
|
||||
legacyKnockbackIsRod = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
public double getNotNullPing() {
|
||||
return ping != null ? ping : platformPlayer.getPing();
|
||||
}
|
||||
|
||||
public double getNotNullPreviousPing() {
|
||||
return previousPing != null ? previousPing : platformPlayer.getPing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the player's ping with compensation for lag spikes.
|
||||
* A hardcoded offset is applied for several reasons,
|
||||
* read the GitHub FAQ before adjusting.
|
||||
*
|
||||
* @return The compensated ping, with a minimum of 1.
|
||||
*/
|
||||
public double getCompensatedPing() {
|
||||
double ping = getNotNullPing();
|
||||
double previousPing = getNotNullPreviousPing();
|
||||
double spikeCompensatedPing = (ping - previousPing > Base.INSTANCE.getConfigManager().getSpikeThreshold()) ? previousPing : ping;
|
||||
|
||||
return Math.max(1, spikeCompensatedPing - PING_OFFSET);
|
||||
}
|
||||
|
||||
public int getCompensatedTicks() {
|
||||
return (int) Math.ceil(getCompensatedPing() * Base.INSTANCE.getTickRate() / 1000); // Multiply ping by seconds per tick
|
||||
}
|
||||
|
||||
public int getTicks() {
|
||||
return (int) Math.ceil(getNotNullPing() * Base.INSTANCE.getTickRate() / 1000); // Multiply ping by seconds per tick
|
||||
}
|
||||
|
||||
public boolean isSpike() {
|
||||
return (getNotNullPing() - getNotNullPreviousPing()) > Base.INSTANCE.getConfigManager().getSpikeThreshold();
|
||||
}
|
||||
|
||||
public void sendPing(boolean async) {
|
||||
if (user == null || user.getEncoderState() != ConnectionState.PLAY) return;
|
||||
|
||||
switch (pingStrategy) {
|
||||
case KEEPALIVE:
|
||||
long keepAliveID = async ? NETTY_THREAD_TRANSACTION_ID : MAIN_THREAD_TRANSACTION_ID;
|
||||
if (async) {
|
||||
ChannelHelper.runInEventLoop(user.getChannel(), () -> {
|
||||
// We call sendPacket instead of writePacket because it flushes immediately
|
||||
// Making our time measurement more accurate since we don't call, System.nanoTime(), wait until flush
|
||||
// And then actually send packet
|
||||
user.sendPacket(new WrapperPlayServerKeepAlive(keepAliveID));
|
||||
});
|
||||
} else {
|
||||
user.sendPacket(new WrapperPlayServerKeepAlive(keepAliveID));
|
||||
}
|
||||
break;
|
||||
case TRANSACTION:
|
||||
PacketWrapper<?> packet;
|
||||
short pingTransactionID = async ? NETTY_THREAD_TRANSACTION_ID : MAIN_THREAD_TRANSACTION_ID;
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17)) {
|
||||
packet = new WrapperPlayServerPing(pingTransactionID);
|
||||
} else {
|
||||
packet = new WrapperPlayServerWindowConfirmation((byte) 0, pingTransactionID, false);
|
||||
}
|
||||
|
||||
// sendPacket (write + flush) rather than writePacket: an unflushed write sits in the channel until the next flush and inflates the measured ping.
|
||||
if (async) {
|
||||
ChannelHelper.runInEventLoop(user.getChannel(), () -> {
|
||||
user.sendPacket(packet);
|
||||
});
|
||||
} else {
|
||||
user.sendPacket(packet);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the Player is on the ground clientside, but not serverside
|
||||
* <p>
|
||||
* Returns <code>ping ≥ (tMax + tFall)</code> and <code>gDist ≤ 1.3</code>
|
||||
* <p>
|
||||
* Where:
|
||||
* <ul>
|
||||
* <li><code>ping</code>: Estimated latency</li>
|
||||
* <li><code>tMax</code>: Time to reach maximum upward velocity</li>
|
||||
* <li><code>tFall</code>: Time to fall to the ground</li>
|
||||
* <li><code>gDist</code>: Distance to the ground</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param verticalVelocity The Player's current vertical velocity.
|
||||
* @return <code>true</code> if the Player is on the ground; <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean isOnGroundClientSide(double verticalVelocity, double distanceToGround) {
|
||||
int tMax = verticalVelocity > 0 ? MathUtil.calculateTimeToMaxVelocity(verticalVelocity, gravityAttribute) : 0;
|
||||
if (tMax == -1)
|
||||
return false; // reached the max tick limit, not safe to predict
|
||||
|
||||
double mH = verticalVelocity > 0 ? MathUtil.calculateDistanceTraveled(verticalVelocity, tMax, gravityAttribute) : 0;
|
||||
int tFall = MathUtil.calculateFallTime(verticalVelocity, mH + distanceToGround, gravityAttribute);
|
||||
|
||||
if (tFall == -1)
|
||||
return false; // reached the max tick limit, not safe to predict
|
||||
|
||||
return (tMax + tFall) - getCompensatedTicks() <= 0 && distanceToGround <= 1.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether or not offGroundSynchronization is enabled for the player.
|
||||
* @return <code>true</code> if enabled; <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean isOffGroundSyncEnabled() {
|
||||
// TODO, per-player offground sync toggles?
|
||||
return ToggleOffGroundSubcommand.offGroundSyncEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the compensated off-ground velocity.
|
||||
* This is used to make knockback feel more accurate off-ground.
|
||||
*
|
||||
* @return Compensated Y axis velocity
|
||||
*/
|
||||
public double getCompensatedOffGroundVelocity() {
|
||||
return MathUtil.getCompensatedVerticalVelocity(platformPlayer.getVelocity().getY(), getGravityAttribute(), getTicks());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ray traces from each corner of the player's bounding box to the ground,
|
||||
* returning the smallest distance, with a maximum limit of 5 blocks.
|
||||
*
|
||||
* @return The distance to the ground in blocks
|
||||
*/
|
||||
public double getDistanceToGround() {
|
||||
double collisionDist = 5;
|
||||
|
||||
PlatformWorld world = platformPlayer.getWorld();
|
||||
|
||||
for (Vector3d corner : getBBCorners()) {
|
||||
RayTraceResult result = world.rayTraceBlocks(corner, new Vector3d(0, -1, 0), 5, FluidHandling.NONE, true);
|
||||
|
||||
if (result == null || result.getHitBlock() == null)
|
||||
continue;
|
||||
|
||||
collisionDist = Math.min(collisionDist, corner.getY() - result.getHitBlockPosition().getY());
|
||||
}
|
||||
|
||||
return collisionDist - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the corners of the Player's bounding box.
|
||||
*
|
||||
* @return An array of locations representing the corners of the bounding box.
|
||||
*/
|
||||
private Vector3d[] getBBCorners() {
|
||||
BoundingBox boundingBox = platformPlayer.getBoundingBox();
|
||||
Vector3d location = platformPlayer.getLocation();
|
||||
|
||||
double adjustment = 0.01; // To ensure the bounding box isn't clipping inside a wall
|
||||
|
||||
return new Vector3d[] {
|
||||
new Vector3d(boundingBox.getMinX() + adjustment, location.getY(), boundingBox.getMinZ() + adjustment),
|
||||
new Vector3d(boundingBox.getMinX() + adjustment, location.getY(), boundingBox.getMaxZ() - adjustment),
|
||||
new Vector3d(boundingBox.getMaxX() - adjustment, location.getY(), boundingBox.getMinZ() + adjustment),
|
||||
new Vector3d(boundingBox.getMaxX() - adjustment, location.getY(), boundingBox.getMaxZ() - adjustment)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the positive vertical velocity.
|
||||
* This is used to switch falling knockback to rising knockback.
|
||||
*
|
||||
* @param attacker The player who is attacking.
|
||||
* @return The calculated positive vertical velocity, consistent with vanilla behavior.
|
||||
*/
|
||||
public double calculateVerticalVelocity(PlatformPlayer attacker) {
|
||||
double yAxis = attacker.getAttackCooldown() > 0.848 ? 0.4 : 0.36080000519752503;
|
||||
|
||||
if (!attacker.isSprinting()) {
|
||||
yAxis = 0.36080000519752503;
|
||||
double resistanceFactor = 0.04000000119 * knockbackResistanceAttribute * 10;
|
||||
yAxis -= resistanceFactor;
|
||||
}
|
||||
|
||||
// vertical velocity is always 0.4 when you have knockback level higher than 0
|
||||
if (attacker.getMainHandKnockbackLevel() > 0)
|
||||
yAxis = 0.4;
|
||||
|
||||
return yAxis;
|
||||
}
|
||||
|
||||
public void updateCombat() {
|
||||
Channel channel = (Channel) user.getChannel();
|
||||
channel.eventLoop().execute(() -> {
|
||||
if (combatTask != null) {
|
||||
combatTask.cancel();
|
||||
}
|
||||
combatTask = newCombatTask(channel);
|
||||
CombatManager.addPlayer(user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the combatTask quit task. This prevents quitCombat() from being called later against a new combat task
|
||||
* Or against a player that has disconnected already
|
||||
* @param async Whether we are already in the players netty thread
|
||||
*/
|
||||
public void quitCombat(boolean async) {
|
||||
Channel channel = (Channel) user.getChannel();
|
||||
Runnable runnable = () -> {
|
||||
if (combatTask != null) {
|
||||
combatTask.cancel();
|
||||
combatTask = null;
|
||||
}
|
||||
CombatManager.removePlayer(user);
|
||||
};
|
||||
if (async) {
|
||||
channel.eventLoop().execute(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private AbstractTaskHandle newCombatTask(Channel channel) {
|
||||
return new NettyTaskHandle(channel.eventLoop().schedule(() -> quitCombat(false), convertTicksToMilliseconds(Base.INSTANCE.getConfigManager().getCombatTimer()), TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
public ClientVersion getClientVersion() {
|
||||
if (user == null)
|
||||
return ClientVersion.UNKNOWN;
|
||||
ClientVersion ver = user.getClientVersion();
|
||||
if (ver == null) {
|
||||
// If temporarily null, assume server version...
|
||||
return ClientVersion.getById(PacketEvents.getAPI().getServerManager().getVersion().getProtocolVersion());
|
||||
}
|
||||
return ver;
|
||||
}
|
||||
|
||||
public long getKeepAliveSendTime() {
|
||||
return lastKeepAliveID;
|
||||
}
|
||||
|
||||
public boolean didWeSendThatPacket(long receivedId) {
|
||||
return receivedId == NETTY_THREAD_TRANSACTION_ID || receivedId == MAIN_THREAD_TRANSACTION_ID;
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onToggledEvent(ToggleOnOffEvent event) {
|
||||
if (!event.getStatus()) {
|
||||
transactionsSent.clear();
|
||||
keepaliveMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@KBSyncEventHandler
|
||||
public void onConfigReloadEvent(ConfigReloadEvent event) {
|
||||
this.pingStrategy = loadPingStrategy(event.getConfigManager());
|
||||
}
|
||||
|
||||
private PingStrategy loadPingStrategy(ConfigManager configManager) {
|
||||
// Default PING: vanilla ignores unintercepted pong echoes, while unintercepted keepalive echoes kick ("Timed out") on 1.20.2+.
|
||||
String pingStrategy = configManager.getConfigWrapper().getString("ping_strategy", "PING");
|
||||
switch (pingStrategy) {
|
||||
case "KEEPALIVE":
|
||||
return PingStrategy.KEEPALIVE;
|
||||
case "PING":
|
||||
case "TRANSACTION":
|
||||
return PingStrategy.TRANSACTION;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown ping_strategy: " + pingStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
private long convertTicksToMilliseconds(long ticks) {
|
||||
return ticks * 50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package me.caseload.knockbacksync.runnable;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.CombatManager;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PingRunnable implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!Base.INSTANCE.getConfigManager().isToggled())
|
||||
return;
|
||||
|
||||
for (User user : CombatManager.getPlayers()) {
|
||||
PlayerData playerData = PlayerDataManager.getPlayerData(user);
|
||||
if (playerData != null)
|
||||
playerData.sendPing(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
public interface AbstractTaskHandle {
|
||||
|
||||
boolean getCancelled();
|
||||
|
||||
void cancel();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
|
||||
public class NettyTaskHandle implements AbstractTaskHandle {
|
||||
|
||||
private final ScheduledFuture scheduledFuture;
|
||||
|
||||
public NettyTaskHandle(ScheduledFuture<?> scheduledFuture) {
|
||||
this.scheduledFuture = scheduledFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getCancelled() {
|
||||
return scheduledFuture.isCancelled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
scheduledFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package me.caseload.knockbacksync.scheduler;
|
||||
|
||||
public interface SchedulerAdapter {
|
||||
AbstractTaskHandle runTask(Runnable task);
|
||||
|
||||
AbstractTaskHandle runTaskAsynchronously(Runnable task);
|
||||
|
||||
AbstractTaskHandle runTaskLater(Runnable task, long delayTicks);
|
||||
|
||||
AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks);
|
||||
|
||||
AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay);
|
||||
|
||||
AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period);
|
||||
|
||||
void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Simple implementation of {@link Sender} using a {@link SenderFactory}
|
||||
*
|
||||
* @param <T> the command sender type
|
||||
*/
|
||||
public final class AbstractSender<T> implements Sender {
|
||||
private final Base plugin;
|
||||
private final SenderFactory<?, T> factory;
|
||||
private final T sender;
|
||||
|
||||
private final UUID uniqueId;
|
||||
private final String name;
|
||||
private final boolean isConsole;
|
||||
|
||||
AbstractSender(Base plugin, SenderFactory<?, T> factory, T sender) {
|
||||
this.plugin = plugin;
|
||||
this.factory = factory;
|
||||
this.sender = sender;
|
||||
this.uniqueId = factory.getUniqueId(this.sender);
|
||||
this.name = factory.getName(this.sender);
|
||||
this.isConsole = this.factory.isConsole(this.sender);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Base getPlugin() {
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUniqueId() {
|
||||
return this.uniqueId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
this.factory.sendMessage(this.sender, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission) {
|
||||
return isConsole() || this.factory.hasPermission(this.sender, permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(String permission, boolean defaultIfUnset) {
|
||||
return isConsole() || this.factory.hasPermission(this.sender, permission, defaultIfUnset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performCommand(String commandLine) {
|
||||
this.factory.performCommand(this.sender, commandLine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConsole() {
|
||||
return this.isConsole;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return true;
|
||||
// return isConsole() || this.plugin.getBootstrap().isPlayerOnline(this.uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
if (!(o instanceof AbstractSender)) return false;
|
||||
final AbstractSender<?> that = (AbstractSender<?>) o;
|
||||
return this.getUniqueId().equals(that.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.uniqueId.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getSender() {
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Wrapper interface to represent a CommandSender/CommandSource within the common command implementations.
|
||||
*/
|
||||
public interface Sender {
|
||||
|
||||
/**
|
||||
* The uuid used by the console sender.
|
||||
*/
|
||||
UUID CONSOLE_UUID = new UUID(0, 0); // 00000000-0000-0000-0000-000000000000
|
||||
/**
|
||||
* The name used by the console sender.
|
||||
*/
|
||||
String CONSOLE_NAME = "Console";
|
||||
|
||||
/**
|
||||
* Gets the plugin instance the sender is from.
|
||||
*
|
||||
* @return the plugin
|
||||
*/
|
||||
Base getPlugin();
|
||||
|
||||
/**
|
||||
* Gets the sender's username
|
||||
*
|
||||
* @return a friendly username for the sender
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Gets a string representing the senders username, and their current location
|
||||
* within the network.
|
||||
*
|
||||
* @return a friendly identifier for the sender
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the sender's unique id.
|
||||
*
|
||||
* <p>See {@link #CONSOLE_UUID} for the console's UUID representation.</p>
|
||||
*
|
||||
* @return the sender's uuid
|
||||
*/
|
||||
UUID getUniqueId();
|
||||
|
||||
/**
|
||||
* Send a json message to the Sender.
|
||||
*
|
||||
* @param message the message to send.
|
||||
*/
|
||||
void sendMessage(String message);
|
||||
|
||||
/**
|
||||
* Check if the Sender has a permission.
|
||||
*
|
||||
* @param permission the permission to check for
|
||||
* @return true if the sender has the permission
|
||||
*/
|
||||
boolean hasPermission(String permission);
|
||||
|
||||
boolean hasPermission(String permission, boolean defaultIfUnset);
|
||||
|
||||
/**
|
||||
* Makes the sender perform a command.
|
||||
*
|
||||
* @param commandLine the command
|
||||
*/
|
||||
void performCommand(String commandLine);
|
||||
|
||||
/**
|
||||
* Gets whether this sender is the console
|
||||
*
|
||||
* @return if the sender is the console
|
||||
*/
|
||||
boolean isConsole();
|
||||
|
||||
/**
|
||||
* Gets whether this sender is still valid & receiving messages.
|
||||
*
|
||||
* @return if this sender is valid
|
||||
*/
|
||||
default boolean isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Object getSender();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package me.caseload.knockbacksync.sender;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Factory class to make a thread-safe sender instance
|
||||
*
|
||||
* @param <P> the plugin type
|
||||
* @param <T> the command sender type
|
||||
*/
|
||||
public abstract class SenderFactory<P extends Base, T> implements AutoCloseable {
|
||||
private final P plugin;
|
||||
|
||||
public SenderFactory(P plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
protected P getPlugin() {
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
protected abstract UUID getUniqueId(T sender);
|
||||
|
||||
protected abstract String getName(T sender);
|
||||
|
||||
protected abstract void sendMessage(T sender, String message);
|
||||
|
||||
protected abstract boolean hasPermission(T sender, String node);
|
||||
|
||||
protected abstract boolean hasPermission(T sender, String node, boolean defaultIfUnset);
|
||||
|
||||
protected abstract void performCommand(T sender, String command);
|
||||
|
||||
protected abstract boolean isConsole(T sender);
|
||||
|
||||
protected boolean shouldSplitNewlines(T sender) {
|
||||
return isConsole(sender);
|
||||
}
|
||||
|
||||
public final Sender wrap(T sender) {
|
||||
Objects.requireNonNull(sender, "sender");
|
||||
return new AbstractSender<>(this.plugin, this, sender);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public final T unwrap(Sender sender) {
|
||||
Objects.requireNonNull(sender, "sender");
|
||||
return (T) sender.getSender();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class AdvancedBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, int[]>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedBarChart(String chartId, Callable<Map<String, int[]>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, int[]> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, int[]> entry : map.entrySet()) {
|
||||
if (entry.getValue().length == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class AdvancedPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public AdvancedPie(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public abstract class CustomChart {
|
||||
|
||||
private final String chartId;
|
||||
|
||||
protected CustomChart(String chartId) {
|
||||
if (chartId == null) {
|
||||
throw new IllegalArgumentException("chartId must not be null");
|
||||
}
|
||||
this.chartId = chartId;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder.JsonObject getRequestJsonObject(
|
||||
BiConsumer<String, Throwable> errorLogger, boolean logErrors) {
|
||||
JsonObjectBuilder builder = new JsonObjectBuilder();
|
||||
builder.appendField("chartId", chartId);
|
||||
try {
|
||||
JsonObjectBuilder.JsonObject data = getChartData();
|
||||
if (data == null) {
|
||||
// If the data is null we don't send the chart.
|
||||
return null;
|
||||
}
|
||||
builder.appendField("data", data);
|
||||
} catch (Throwable t) {
|
||||
if (logErrors) {
|
||||
errorLogger.accept("Failed to get data for custom chart with id " + chartId, t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class DrilldownPie extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Map<String, Integer>>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public DrilldownPie(String chartId, Callable<Map<String, Map<String, Integer>>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Map<String, Integer>> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean reallyAllSkipped = true;
|
||||
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
||||
JsonObjectBuilder valueBuilder = new JsonObjectBuilder();
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
||||
valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue());
|
||||
allSkipped = false;
|
||||
}
|
||||
if (!allSkipped) {
|
||||
reallyAllSkipped = false;
|
||||
valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build());
|
||||
}
|
||||
}
|
||||
if (reallyAllSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An extremely simple JSON builder.
|
||||
*
|
||||
* <p>While this class is neither feature-rich nor the most performant one, it's sufficient enough
|
||||
* for its use-case.
|
||||
*/
|
||||
public class JsonObjectBuilder {
|
||||
|
||||
private StringBuilder builder = new StringBuilder();
|
||||
|
||||
private boolean hasAtLeastOneField = false;
|
||||
|
||||
public JsonObjectBuilder() {
|
||||
builder.append("{");
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt.
|
||||
*
|
||||
* <p>This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'.
|
||||
* Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n").
|
||||
*
|
||||
* @param value The value to escape.
|
||||
* @return The escaped value.
|
||||
*/
|
||||
private static String escape(String value) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '"') {
|
||||
builder.append("\\\"");
|
||||
} else if (c == '\\') {
|
||||
builder.append("\\\\");
|
||||
} else if (c <= '\u000F') {
|
||||
builder.append("\\u000").append(Integer.toHexString(c));
|
||||
} else if (c <= '\u001F') {
|
||||
builder.append("\\u00").append(Integer.toHexString(c));
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a null field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendNull(String key) {
|
||||
appendFieldUnescaped(key, "null");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param value The value of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, String value) {
|
||||
if (value == null) {
|
||||
throw new IllegalArgumentException("JSON value must not be null");
|
||||
}
|
||||
appendFieldUnescaped(key, "\"" + escape(value) + "\"");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an integer field to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param value The value of the field.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, int value) {
|
||||
appendFieldUnescaped(key, String.valueOf(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an object to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param object The object.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, JsonObject object) {
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("JSON object must not be null");
|
||||
}
|
||||
appendFieldUnescaped(key, object.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The string array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, String[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values)
|
||||
.map(value -> "\"" + escape(value) + "\"")
|
||||
.collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an integer array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The integer array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, int[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an object array to the JSON.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param values The integer array.
|
||||
* @return A reference to this object.
|
||||
*/
|
||||
public JsonObjectBuilder appendField(String key, JsonObject[] values) {
|
||||
if (values == null) {
|
||||
throw new IllegalArgumentException("JSON values must not be null");
|
||||
}
|
||||
String escapedValues =
|
||||
Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(","));
|
||||
appendFieldUnescaped(key, "[" + escapedValues + "]");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a field to the object.
|
||||
*
|
||||
* @param key The key of the field.
|
||||
* @param escapedValue The escaped value of the field.
|
||||
*/
|
||||
private void appendFieldUnescaped(String key, String escapedValue) {
|
||||
if (builder == null) {
|
||||
throw new IllegalStateException("JSON has already been built");
|
||||
}
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("JSON key must not be null");
|
||||
}
|
||||
if (hasAtLeastOneField) {
|
||||
builder.append(",");
|
||||
}
|
||||
builder.append("\"").append(escape(key)).append("\":").append(escapedValue);
|
||||
hasAtLeastOneField = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the JSON string and invalidates this builder.
|
||||
*
|
||||
* @return The built JSON string.
|
||||
*/
|
||||
public JsonObject build() {
|
||||
if (builder == null) {
|
||||
throw new IllegalStateException("JSON has already been built");
|
||||
}
|
||||
JsonObject object = new JsonObject(builder.append("}").toString());
|
||||
builder = null;
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* A super simple representation of a JSON object.
|
||||
*
|
||||
* <p>This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not
|
||||
* allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String,
|
||||
* JsonObject)}.
|
||||
*/
|
||||
public static class JsonObject {
|
||||
|
||||
private final String value;
|
||||
|
||||
private JsonObject(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class MultiLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public MultiLineChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
boolean allSkipped = true;
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
// Skip this invalid
|
||||
continue;
|
||||
}
|
||||
allSkipped = false;
|
||||
valuesBuilder.appendField(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (allSkipped) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class SimpleBarChart extends CustomChart {
|
||||
|
||||
private final Callable<Map<String, Integer>> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimpleBarChart(String chartId, Callable<Map<String, Integer>> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
JsonObjectBuilder valuesBuilder = new JsonObjectBuilder();
|
||||
Map<String, Integer> map = callable.call();
|
||||
if (map == null || map.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
valuesBuilder.appendField(entry.getKey(), new int[]{entry.getValue()});
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class SimplePie extends CustomChart {
|
||||
|
||||
private final Callable<String> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SimplePie(String chartId, Callable<String> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
String value = callable.call();
|
||||
if (value == null || value.isEmpty()) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class SingleLineChart extends CustomChart {
|
||||
|
||||
private final Callable<Integer> callable;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param chartId The id of the chart.
|
||||
* @param callable The callable which is used to request the chart data.
|
||||
*/
|
||||
public SingleLineChart(String chartId, Callable<Integer> callable) {
|
||||
super(chartId);
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObjectBuilder.JsonObject getChartData() throws Exception {
|
||||
int value = callable.call();
|
||||
if (value == 0) {
|
||||
// Null = skip the chart
|
||||
return null;
|
||||
}
|
||||
return new JsonObjectBuilder().appendField("value", value).build();
|
||||
}
|
||||
}
|
||||
+23
-39
@@ -1,30 +1,25 @@
|
||||
package me.caseload.knockbacksync.stats;
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import me.caseload.knockbacksync.KnockbackSync;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bukkit.Bukkit;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.common.BuildConfig;
|
||||
import me.caseload.knockbacksync.stats.SimplePie;
|
||||
import org.kohsuke.github.GHAsset;
|
||||
import org.kohsuke.github.GHRelease;
|
||||
import org.kohsuke.github.GitHub;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
|
||||
public class BuildTypePie extends SimplePie {
|
||||
|
||||
private static final String RELEASES_FILE = "releases.txt";
|
||||
private static final String DEV_BUILDS_FILE = "dev-builds.txt";
|
||||
private static final File dataFolder = KnockbackSync.INSTANCE.getDataFolder();
|
||||
private static final File dataFolder = Base.INSTANCE.getDataFolder();
|
||||
private static String cachedBuildType = null;
|
||||
|
||||
public BuildTypePie() {
|
||||
@@ -40,7 +35,7 @@ public class BuildTypePie extends SimplePie {
|
||||
|
||||
private static String calculateBuildType() {
|
||||
try {
|
||||
String currentHash = getPluginJarHash();
|
||||
String currentHash = Base.INSTANCE.getPluginJarHashProvider().getPluginJarHash();
|
||||
downloadBuildFiles();
|
||||
|
||||
if (isHashInFile(currentHash, new File(dataFolder, RELEASES_FILE))) {
|
||||
@@ -58,23 +53,26 @@ public class BuildTypePie extends SimplePie {
|
||||
|
||||
private static void downloadBuildFiles() throws IOException {
|
||||
GitHub gitHub = GitHub.connectAnonymously();
|
||||
GHRelease latestRelease = gitHub.getRepository("Axionize/knockback-sync")
|
||||
GHRelease latestRelease = gitHub.getRepository(BuildConfig.GITHUB_REPO)
|
||||
.getLatestRelease();
|
||||
List<GHAsset> assets = latestRelease.listAssets().toList();
|
||||
for (GHAsset asset : assets) {
|
||||
if (asset.getName().equals(RELEASES_FILE) || asset.getName().equals(DEV_BUILDS_FILE)) {
|
||||
KnockbackSync.INSTANCE.getLogger().info("Downloading: " + asset.getName());
|
||||
Base.INSTANCE.getLogger().info("Downloading: " + asset.getName());
|
||||
|
||||
String jsonContent = readStringFromURL(asset.getUrl().toString());
|
||||
JsonObject jsonObject = JsonParser.parseString(jsonContent).getAsJsonObject();
|
||||
String downloadUrl = jsonObject.get("browser_download_url").getAsString();
|
||||
|
||||
try (InputStream inputStream = new URL(downloadUrl).openStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(dataFolder, asset.getName()))) {
|
||||
inputStream.transferTo(outputStream);
|
||||
try (InputStream inputStream = new URL(downloadUrl).openStream(); FileOutputStream outputStream = new FileOutputStream(new File(dataFolder, asset.getName()))) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
KnockbackSync.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
|
||||
Base.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,29 +85,15 @@ public class BuildTypePie extends SimplePie {
|
||||
return lines.contains(hash);
|
||||
}
|
||||
|
||||
private static String getPluginJarHash() throws Exception {
|
||||
URL jarUrl = Bukkit.getPluginManager().getPlugin("KnockbackSync").getClass().getProtectionDomain().getCodeSource().getLocation();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (InputStream is = jarUrl.openStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
byte[] hash = digest.digest();
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
private static String readStringFromURL(String urlString) throws IOException {
|
||||
try (InputStream inputStream = new URL(urlString).openStream()) {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
result.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return result.toString("UTF-8");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.stats.AdvancedPie;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClientBrandsPie extends AdvancedPie {
|
||||
|
||||
// Gets the client versions of players online
|
||||
public ClientBrandsPie() {
|
||||
super("client_brands", () -> {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (PlatformPlayer player : Base.INSTANCE.getPlatformServer().getOnlinePlayers()) {
|
||||
User user = player.getUser();
|
||||
if (user == null || user.getClientVersion() == null) {
|
||||
valueMap.put("vanilla", valueMap.getOrDefault("vanilla", 0) + 1);
|
||||
} else {
|
||||
valueMap.put(player.getClientBrand(), valueMap.getOrDefault(player.getClientBrand(), 0) + 1);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import me.caseload.knockbacksync.stats.CustomChart;
|
||||
|
||||
public interface Metrics {
|
||||
|
||||
void addCustomChart(CustomChart chart);
|
||||
void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import me.caseload.knockbacksync.stats.CustomChart;
|
||||
import me.caseload.knockbacksync.stats.JsonObjectBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
public class MetricsBase {
|
||||
|
||||
/**
|
||||
* The version of the Metrics class.
|
||||
*/
|
||||
public static final String METRICS_VERSION = "3.1.0";
|
||||
|
||||
private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private final String platform;
|
||||
|
||||
private final String serverUuid;
|
||||
|
||||
private final int serviceId;
|
||||
|
||||
private final Consumer<JsonObjectBuilder> appendPlatformDataConsumer;
|
||||
|
||||
private final Consumer<JsonObjectBuilder> appendServiceDataConsumer;
|
||||
|
||||
private final Consumer<Runnable> submitTaskConsumer;
|
||||
|
||||
private final Supplier<Boolean> checkServiceEnabledSupplier;
|
||||
|
||||
private final BiConsumer<String, Throwable> errorLogger;
|
||||
|
||||
private final Consumer<String> infoLogger;
|
||||
|
||||
private final boolean logErrors;
|
||||
|
||||
private final boolean logSentData;
|
||||
|
||||
private final boolean logResponseStatusText;
|
||||
|
||||
private final Set<CustomChart> customCharts = new HashSet<>();
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
/**
|
||||
* Creates a new MetricsBase class instance.
|
||||
*
|
||||
* @param platform The platform of the service.
|
||||
* @param serviceId The id of the service.
|
||||
* @param serverUuid The server uuid.
|
||||
* @param enabled Whether or not data sending is enabled.
|
||||
* @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||
* appends all platform-specific data.
|
||||
* @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
|
||||
* appends all service-specific data.
|
||||
* @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
|
||||
* used to delegate the data collection to a another thread to prevent errors caused by
|
||||
* concurrency. Can be {@code null}.
|
||||
* @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
|
||||
* @param errorLogger A consumer that accepts log message and an error.
|
||||
* @param infoLogger A consumer that accepts info log messages.
|
||||
* @param logErrors Whether or not errors should be logged.
|
||||
* @param logSentData Whether or not the sent data should be logged.
|
||||
* @param logResponseStatusText Whether or not the response status text should be logged.
|
||||
* @param skipRelocateCheck Whether or not the relocate check should be skipped.
|
||||
*/
|
||||
public MetricsBase(
|
||||
String platform,
|
||||
String serverUuid,
|
||||
int serviceId,
|
||||
boolean enabled,
|
||||
Consumer<JsonObjectBuilder> appendPlatformDataConsumer,
|
||||
Consumer<JsonObjectBuilder> appendServiceDataConsumer,
|
||||
Consumer<Runnable> submitTaskConsumer,
|
||||
Supplier<Boolean> checkServiceEnabledSupplier,
|
||||
BiConsumer<String, Throwable> errorLogger,
|
||||
Consumer<String> infoLogger,
|
||||
boolean logErrors,
|
||||
boolean logSentData,
|
||||
boolean logResponseStatusText,
|
||||
boolean skipRelocateCheck) {
|
||||
this.scheduler = getScheduledThreadPoolExecutor();
|
||||
this.platform = platform;
|
||||
this.serverUuid = serverUuid;
|
||||
this.serviceId = serviceId;
|
||||
this.enabled = enabled;
|
||||
this.appendPlatformDataConsumer = appendPlatformDataConsumer;
|
||||
this.appendServiceDataConsumer = appendServiceDataConsumer;
|
||||
this.submitTaskConsumer = submitTaskConsumer;
|
||||
this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
|
||||
this.errorLogger = errorLogger;
|
||||
this.infoLogger = infoLogger;
|
||||
this.logErrors = logErrors;
|
||||
this.logSentData = logSentData;
|
||||
this.logResponseStatusText = logResponseStatusText;
|
||||
if (!skipRelocateCheck) {
|
||||
checkRelocation();
|
||||
}
|
||||
if (enabled) {
|
||||
// WARNING: Removing the option to opt-out will get your plugin banned from
|
||||
// bStats
|
||||
startSubmitting();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor() {
|
||||
ScheduledThreadPoolExecutor scheduler =
|
||||
new ScheduledThreadPoolExecutor(
|
||||
1,
|
||||
task -> {
|
||||
Thread thread = new Thread(task, "bStats-Metrics");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
// We want delayed tasks (non-periodic) that will execute in the future to be
|
||||
// cancelled when the scheduler is shutdown.
|
||||
// Otherwise, we risk preventing the server from shutting down even when
|
||||
// MetricsBase#shutdown() is called
|
||||
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gzips the given string.
|
||||
*
|
||||
* @param str The string to gzip.
|
||||
* @return The gzipped string.
|
||||
*/
|
||||
private static byte[] compress(final String str) throws IOException {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
|
||||
gzip.write(str.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
public void addCustomChart(CustomChart chart) {
|
||||
this.customCharts.add(chart);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
private void startSubmitting() {
|
||||
final Runnable submitTask =
|
||||
() -> {
|
||||
if (!enabled || !checkServiceEnabledSupplier.get()) {
|
||||
// Submitting data or service is disabled
|
||||
scheduler.shutdown();
|
||||
return;
|
||||
}
|
||||
if (submitTaskConsumer != null) {
|
||||
submitTaskConsumer.accept(this::submitData);
|
||||
} else {
|
||||
this.submitData();
|
||||
}
|
||||
};
|
||||
// Many servers tend to restart at a fixed time at xx:00 which causes an uneven
|
||||
// distribution of requests on the
|
||||
// bStats backend. To circumvent this problem, we introduce some randomness into
|
||||
// the initial and second delay.
|
||||
// WARNING: You must not modify and part of this Metrics class, including the
|
||||
// submit delay or frequency!
|
||||
// WARNING: Modifying this code will get your plugin banned on bStats. Just
|
||||
// don't do it!
|
||||
long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
|
||||
long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
|
||||
scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
|
||||
scheduler.scheduleAtFixedRate(
|
||||
submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void submitData() {
|
||||
final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
|
||||
appendPlatformDataConsumer.accept(baseJsonBuilder);
|
||||
final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
|
||||
appendServiceDataConsumer.accept(serviceJsonBuilder);
|
||||
JsonObjectBuilder.JsonObject[] chartData =
|
||||
customCharts.stream()
|
||||
.map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(JsonObjectBuilder.JsonObject[]::new);
|
||||
serviceJsonBuilder.appendField("id", serviceId);
|
||||
serviceJsonBuilder.appendField("customCharts", chartData);
|
||||
baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
|
||||
baseJsonBuilder.appendField("serverUUID", serverUuid);
|
||||
baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
|
||||
JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
|
||||
scheduler.execute(
|
||||
() -> {
|
||||
try {
|
||||
// Send the data
|
||||
sendData(data);
|
||||
} catch (Exception e) {
|
||||
// Something went wrong! :(
|
||||
if (logErrors) {
|
||||
errorLogger.accept("Could not submit bStats metrics data", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendData(JsonObjectBuilder.JsonObject data) throws Exception {
|
||||
if (logSentData) {
|
||||
infoLogger.accept("Sent bStats metrics data: " + data.toString());
|
||||
}
|
||||
String url = String.format(REPORT_URL, platform);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
// Compress the data to save bandwidth
|
||||
byte[] compressedData = compress(data.toString());
|
||||
connection.setRequestMethod("POST");
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
connection.addRequestProperty("Connection", "close");
|
||||
connection.addRequestProperty("Content-Encoding", "gzip");
|
||||
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("User-Agent", "Metrics-Service/1");
|
||||
connection.setDoOutput(true);
|
||||
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
|
||||
outputStream.write(compressedData);
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try (BufferedReader bufferedReader =
|
||||
new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
}
|
||||
if (logResponseStatusText) {
|
||||
infoLogger.accept("Sent data to bStats and received response: " + builder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the class was properly relocated.
|
||||
*/
|
||||
private void checkRelocation() {
|
||||
// You can use the property to disable the check in your test environment
|
||||
if (System.getProperty("bstats.relocatecheck") == null
|
||||
|| !System.getProperty("bstats.relocatecheck").equals("false")) {
|
||||
// Maven's Relocate is clever and changes strings, too. So we have to use this
|
||||
// little "trick" ... :D
|
||||
final String defaultPackage =
|
||||
new String(new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
|
||||
final String examplePackage =
|
||||
new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
|
||||
// We want to make sure no one just copy & pastes the example and uses the wrong
|
||||
// package names
|
||||
if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
|
||||
|| MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
|
||||
throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.User;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
import me.caseload.knockbacksync.manager.PlayerDataManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
import me.caseload.knockbacksync.stats.AdvancedPie;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PlayerVersionsPie extends AdvancedPie {
|
||||
|
||||
// Gets the client versions of players online
|
||||
public PlayerVersionsPie() {
|
||||
super("player_version", () -> {
|
||||
Map<String, Integer> valueMap = new HashMap<>();
|
||||
for (PlatformPlayer player : Base.INSTANCE.getPlatformServer().getOnlinePlayers()) {
|
||||
User user = player.getUser();
|
||||
if (user == null || user.getClientVersion() == null) {
|
||||
valueMap.put(ClientVersion.UNKNOWN.toString(), valueMap.getOrDefault(ClientVersion.UNKNOWN.toString(), 0) + 1);
|
||||
} else {
|
||||
valueMap.put(user.getClientVersion().toString(), valueMap.getOrDefault(user.getClientVersion().toString(), 0) + 1);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
public class PluginJarHashProvider {
|
||||
|
||||
protected final URL jarURL;
|
||||
|
||||
public PluginJarHashProvider(URL jarURL) {
|
||||
this.jarURL = jarURL;
|
||||
}
|
||||
|
||||
public String getPluginJarHash() throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
try (InputStream is = jarURL.openStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = is.read(buffer)) > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
byte[] hash = digest.digest();
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package me.caseload.knockbacksync.stats.custom;
|
||||
|
||||
import lombok.Getter;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
@Getter
|
||||
public abstract class StatsManager {
|
||||
|
||||
Metrics metrics;
|
||||
|
||||
public StatsManager(Metrics metrics) {
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
Base.INSTANCE.getScheduler().runTaskAsynchronously(() -> {
|
||||
BuildTypePie.determineBuildType(); // Function to calculate hash
|
||||
metrics.addCustomChart(new PlayerVersionsPie());
|
||||
metrics.addCustomChart(new BuildTypePie());
|
||||
metrics.addCustomChart(new ClientBrandsPie());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ChatUtil {
|
||||
|
||||
public static @NotNull String translateAlternateColorCodes(char altColorChar, @NotNull String textToTranslate) {
|
||||
Preconditions.checkArgument(textToTranslate != null, "Cannot translate null text");
|
||||
char[] b = textToTranslate.toCharArray();
|
||||
|
||||
for (int i = 0; i < b.length - 1; ++i) {
|
||||
if (b[i] == altColorChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx".indexOf(b[i + 1]) > -1) {
|
||||
b[i] = 167;
|
||||
b[i + 1] = Character.toLowerCase(b[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
return new String(b);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,64 @@
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.util.reflection.Reflection;
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GeyserUtil {
|
||||
private static boolean CHECKED_FOR_GEYSER = false;
|
||||
private static boolean GEYSER_PRESENT = false;
|
||||
private static Class<?> GEYSER_CLASS;
|
||||
private static Class<?> GEYSER_API_CLASS;
|
||||
private static Method GEYSER_API_METHOD;
|
||||
private static Method CONNECTION_BY_UUID_METHOD;
|
||||
|
||||
public static boolean isGeyserPlayer(UUID uuid) {
|
||||
if (!CHECKED_FOR_GEYSER) {
|
||||
try {
|
||||
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
|
||||
GEYSER_CLASS = classLoader.loadClass("org.geysermc.api.Geyser");
|
||||
GEYSER_PRESENT = true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
GEYSER_PRESENT = false;
|
||||
}
|
||||
CHECKED_FOR_GEYSER = true;
|
||||
}
|
||||
|
||||
if (GEYSER_PRESENT) {
|
||||
if (GEYSER_API_CLASS == null) {
|
||||
try {
|
||||
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
|
||||
GEYSER_API_CLASS = classLoader.loadClass("org.geysermc.api.GeyserApiBase");
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
if (GEYSER_API_METHOD == null) {
|
||||
GEYSER_API_METHOD = Reflection.getMethodExact(GEYSER_CLASS, "api", null);
|
||||
}
|
||||
if (CONNECTION_BY_UUID_METHOD == null) {
|
||||
CONNECTION_BY_UUID_METHOD = Reflection.getMethod(GEYSER_API_CLASS, "connectionByUuid", 0);
|
||||
}
|
||||
Object apiInstance = null;
|
||||
try {
|
||||
apiInstance = GEYSER_API_METHOD.invoke(null);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Object connection = null;
|
||||
try {
|
||||
if (apiInstance != null) {
|
||||
connection = CONNECTION_BY_UUID_METHOD.invoke(apiInstance, uuid);
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return connection != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Legacy (Minecraft 1.8) melee knockback formula.
|
||||
*
|
||||
* This file is a port of the knockback calculation from BukkitOldCombatMechanics
|
||||
* (kernitus/BukkitOldCombatMechanics), module ModulePlayerKnockback, which is
|
||||
* licensed under the Mozilla Public License, v. 2.0:
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* The MPL-2.0 does not carry the "Incompatible With Secondary Licenses" notice
|
||||
* (Exhibit B), so per MPL-2.0 §3.3 this Covered Software may be distributed as
|
||||
* part of a Larger Work under the GNU General Public License v3.0-or-later that
|
||||
* governs the rest of KnockbackSync. The terms of the MPL continue to apply to
|
||||
* this file. See the NOTICE file at the project root for attribution details.
|
||||
*/
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import me.caseload.knockbacksync.manager.ConfigManager;
|
||||
import me.caseload.knockbacksync.player.PlatformPlayer;
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
|
||||
/**
|
||||
* Computes the exact 1.8-style melee knockback vector applied to a victim, so that
|
||||
* KnockbackSync can own the final melee velocity (horizontal + vertical) and then
|
||||
* latency-correct it, rather than letting vanilla knockback win.
|
||||
*
|
||||
* <p>The math is a faithful reproduction of OldCombatMechanics' legacy player
|
||||
* knockback so that values are identical to running OCM's old-player-knockback
|
||||
* module, while KnockbackSync remains the single writer of the velocity.</p>
|
||||
*/
|
||||
public final class LegacyKnockback {
|
||||
|
||||
private LegacyKnockback() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param victim the player receiving knockback
|
||||
* @param attacker the attacking player
|
||||
* @param victimData the victim's tracked data (for knockback resistance attribute)
|
||||
* @param config the plugin configuration (legacy knockback tunables)
|
||||
* @return the 1.8-style velocity to apply to the victim, before latency Y-correction
|
||||
*/
|
||||
public static Vector3d compute(PlatformPlayer victim, PlatformPlayer attacker,
|
||||
PlayerData victimData, ConfigManager config) {
|
||||
final double knockbackHorizontal = config.getLegacyKnockbackHorizontal();
|
||||
final double knockbackVertical = config.getLegacyKnockbackVertical();
|
||||
final double knockbackVerticalLimit = config.getLegacyKnockbackVerticalLimit();
|
||||
final double knockbackExtraHorizontal = config.getLegacyKnockbackExtraHorizontal();
|
||||
final double knockbackExtraVertical = config.getLegacyKnockbackExtraVertical();
|
||||
final boolean netheriteKnockbackResistance = config.isLegacyKnockbackResistanceEnabled();
|
||||
|
||||
// Figure out base knockback direction (attacker -> victim) with vanilla jitter
|
||||
// when the two are stacked on the exact same column.
|
||||
double d0 = attacker.getX() - victim.getX();
|
||||
double d1;
|
||||
for (d1 = attacker.getZ() - victim.getZ(); d0 * d0 + d1 * d1 < 1.0E-4D;
|
||||
d1 = (Math.random() - Math.random()) * 0.01D) {
|
||||
d0 = (Math.random() - Math.random()) * 0.01D;
|
||||
}
|
||||
|
||||
final double magnitude = Math.sqrt(d0 * d0 + d1 * d1);
|
||||
|
||||
// Player velocity before any friction is applied (pre-knockback motion).
|
||||
final Vector3d v = victim.getVelocity();
|
||||
|
||||
// Apply friction (halve), then add base knockback.
|
||||
double vx = (v.getX() / 2) - (d0 / magnitude * knockbackHorizontal);
|
||||
double vy = (v.getY() / 2) + knockbackVertical;
|
||||
double vz = (v.getZ() / 2) - (d1 / magnitude * knockbackHorizontal);
|
||||
|
||||
// Bonus knockback for sprinting and the Knockback enchantment.
|
||||
int bonusKnockback = attacker.getWeaponKnockbackLevel();
|
||||
if (attacker.isSprinting()) {
|
||||
bonusKnockback++;
|
||||
}
|
||||
|
||||
// NOTE: vanilla caps vertical BEFORE adding the sprint/enchant bonus vertical.
|
||||
if (vy > knockbackVerticalLimit) {
|
||||
vy = knockbackVerticalLimit;
|
||||
}
|
||||
|
||||
if (bonusKnockback > 0) {
|
||||
final float yaw = attacker.getYaw();
|
||||
vx += -Math.sin(yaw * 3.1415927F / 180.0F) * (float) bonusKnockback * knockbackExtraHorizontal;
|
||||
vy += knockbackExtraVertical;
|
||||
vz += Math.cos(yaw * 3.1415927F / 180.0F) * (float) bonusKnockback * knockbackExtraHorizontal;
|
||||
}
|
||||
|
||||
if (netheriteKnockbackResistance) {
|
||||
// Each piece of netherite armour yields 10% horizontal resistance.
|
||||
final double resistance = 1 - victimData.getKnockbackResistanceAttribute();
|
||||
vx *= resistance;
|
||||
vz *= resistance;
|
||||
}
|
||||
|
||||
return new Vector3d(vx, vy, vz);
|
||||
}
|
||||
}
|
||||
+74
-59
@@ -1,60 +1,75 @@
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
private static final double TERMINAL_VELOCITY = 3.92;
|
||||
private static final double MULTIPLIER = 0.98;
|
||||
private static final int MAX_TICKS = 20;
|
||||
|
||||
public static double calculateDistanceTraveled(double velocity, int time, double acceleration)
|
||||
{
|
||||
double totalDistance = 0;
|
||||
|
||||
for (int i = 0; i < time; i++)
|
||||
{
|
||||
totalDistance += velocity;
|
||||
velocity = ((velocity - acceleration) * MULTIPLIER);
|
||||
velocity = Math.min(velocity, TERMINAL_VELOCITY);
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
public static int calculateFallTime(double initialVelocity, double distance, double acceleration) {
|
||||
double velocity = Math.abs(initialVelocity);
|
||||
int ticks = 0;
|
||||
|
||||
while (distance > 0) {
|
||||
if (ticks > MAX_TICKS)
|
||||
return -1;
|
||||
|
||||
velocity += acceleration;
|
||||
velocity = Math.min(velocity, TERMINAL_VELOCITY);
|
||||
velocity *= MULTIPLIER;
|
||||
distance -= velocity;
|
||||
ticks++;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
public static int calculateTimeToMaxVelocity(double targetVerticalVelocity, double acceleration) {
|
||||
double a = -acceleration * MULTIPLIER;
|
||||
double b = acceleration + TERMINAL_VELOCITY * MULTIPLIER;
|
||||
double c = -2 * targetVerticalVelocity;
|
||||
|
||||
double discriminant = b * b - 4 * a * c;
|
||||
if (discriminant < 0)
|
||||
return 0;
|
||||
|
||||
double positiveRoot = (-b + Math.sqrt(discriminant)) / (2 * a);
|
||||
return (int) Math.ceil(positiveRoot * 20);
|
||||
}
|
||||
|
||||
public static double clamp(double num, double min, double max) {
|
||||
if (num < min)
|
||||
return min;
|
||||
|
||||
return Math.min(num, max);
|
||||
}
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import me.caseload.knockbacksync.player.PlayerData;
|
||||
|
||||
import me.caseload.knockbacksync.Base;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
private static final double TERMINAL_VELOCITY = 3.92;
|
||||
private static final double MULTIPLIER = 0.98;
|
||||
private static final int MAX_TICKS = 30;
|
||||
|
||||
public static double getCompensatedVerticalVelocity(double velocity, double acceleration, int ticks) {
|
||||
while (ticks > 0) {
|
||||
velocity -= acceleration;
|
||||
velocity *= MULTIPLIER;
|
||||
ticks--;
|
||||
}
|
||||
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public static double calculateDistanceTraveled(double velocity, int time, double acceleration) {
|
||||
double totalDistance = 0;
|
||||
|
||||
for (int i = 0; i < time; i++) {
|
||||
totalDistance += velocity;
|
||||
velocity = ((velocity - acceleration) * MULTIPLIER);
|
||||
velocity = Math.min(velocity, TERMINAL_VELOCITY);
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
public static int calculateFallTime(double initialVelocity, double distance, double acceleration) {
|
||||
double velocity = Math.abs(initialVelocity);
|
||||
int ticks = 0;
|
||||
|
||||
while (distance > 0) {
|
||||
if (ticks > MAX_TICKS)
|
||||
return -1;
|
||||
|
||||
velocity += acceleration;
|
||||
velocity = Math.min(velocity, TERMINAL_VELOCITY);
|
||||
velocity *= MULTIPLIER;
|
||||
distance -= velocity;
|
||||
ticks++;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
public static int calculateTimeToMaxVelocity(double velocity, double acceleration) {
|
||||
int ticks = 0;
|
||||
|
||||
while (velocity > 0) {
|
||||
if (ticks > MAX_TICKS)
|
||||
return -1;
|
||||
|
||||
velocity -= acceleration;
|
||||
velocity = Math.min(velocity, TERMINAL_VELOCITY);
|
||||
velocity *= MULTIPLIER;
|
||||
ticks++;
|
||||
}
|
||||
|
||||
return ticks;
|
||||
}
|
||||
|
||||
public static double clamp(double num, double min, double max) {
|
||||
if (num < min)
|
||||
return min;
|
||||
|
||||
return Math.min(num, max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package me.caseload.knockbacksync.util;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class NumberConversions {
|
||||
public static long toLong(@Nullable Object object, long defaultValue) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).longValue();
|
||||
} else {
|
||||
try {
|
||||
return Long.parseLong(object.toString());
|
||||
} catch (NumberFormatException | NullPointerException ignored) {
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static int toInt(@Nullable Object object, int def) {
|
||||
if (object instanceof Number) {
|
||||
return ((Number) object).intValue();
|
||||
} else {
|
||||
try {
|
||||
return Integer.parseInt(object.toString());
|
||||
} catch (NumberFormatException | NullPointerException ignored) {
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package me.caseload.knockbacksync.util.data;
|
||||
|
||||
public interface MethodFinder {
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.caseload.knockbacksync.util.data;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
public class Pair<A, B> {
|
||||
private final A first;
|
||||
private final B second;
|
||||
|
||||
public Pair(A first, B second) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
public static <T, K> Pair<T, K> of(T a, K b) {
|
||||
return new Pair<T, K>(a, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Pair)) {
|
||||
return false;
|
||||
}
|
||||
Pair b = (Pair) o;
|
||||
return Objects.equals(this.first, b.first) && Objects.equals(this.second, b.second);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user