146 Commits
Author SHA1 Message Date
caseload 58e5a680ce Merge pull request #39 from Krymonota/patch-1
Fix ineffectiveness of off-ground synchronization config option
2025-08-08 23:27:05 -05:00
Niklas Gerstner 4fbb3bad11 Fix ineffectiveness of off-ground synchronization config option 2025-06-20 20:39:42 +02:00
caseload 74b94f0988 Merge pull request #37 from Axionize/dev
Fabric Updates to 1.21.5 and fix numerous NPEs; Remove Fabric Bstats
2025-05-19 17:18:26 -05:00
Axionize 4a5543da2a Remove Fabric bstats 2025-04-29 14:54:34 -04:00
Axionize 5a8682dc26 Bump PE for 1.21.5 Update 2025-04-20 22:23:23 -04:00
Axionize 0ed4e78afd Fix actions buildscript 2025-04-20 22:17:43 -04:00
Axionize 6b049d98b0 Update gradle wrapper 2025-04-20 22:14:49 -04:00
Axionize 791e0575a9 Fix another rare NPE in PingRunnable 2025-04-20 21:57:06 -04:00
caseload 491e9d7d76 Merge pull request #36 from bruhetan/main
Remove Stray PvP from the server list
2025-04-04 18:27:45 -05:00
Axionize 35faf08cfa Common:
- Fix rare NPE occurs when players disconnect while KB is being processed or a command is being run

Fabric:
- Check if native player object is null before constructing PlatformPlayer
2025-04-01 05:25:43 -04:00
Axionize 622c23a729 Bukkit:
- Add new build option -Prelocate / RELOCATE_JAR that defines whether to relocate dependencies. Make the default KB Sync jar relocate all deps.

Fabric:
- Clean up build script
- bump cloud-fabric-command version
2025-03-29 05:38:53 -04:00
bruhetan aa45b4797d Remove EU Stray from server list 2025-03-24 15:12:39 -04:00
bruhetan dee7dc9984 Remove Stray from server list 2025-03-24 15:12:19 -04:00
Axionize 2eba6ec2ab Add support to stop erroring on Carpet Mod fake players 2025-03-23 10:06:00 -04:00
Axionize 8783dd0683 Migrate to yarn mappings + 1.21.4 Update 2025-03-23 09:54:59 -04:00
Axionize e861daa8c1 Fabric:
- Fix Geyser detection
- Force rebuild with new PE
- Build with only used FabricAPI modules
2025-03-23 08:15:30 -04:00
Axionize c970c1af6f Disable manually loading instance of FabricAPI; let PE dep do it 2025-03-23 03:05:44 -04:00
Axionize 921c009a59 Fix client brand NPE 2025-03-23 00:45:19 -04:00
Axionize ea0c2736e1 Fix Java 8 compatability in updater 2025-03-17 05:50:59 -04:00
Axionize 25411ba54f Common:
- Major internal refactor to use PE user object

Folia:
- Fix sometimes NPEing on Disconnect

Fabric:
- Fix default client brand
2025-03-17 05:42:32 -04:00
Axionize 715013d90f Refactor login event to use UserLoginEvent() with PE, prevents unneccessary stacktrace from PE join errors 2025-03-16 00:56:55 -04:00
Axionize a59ff6e625 Common:
- Fix NPE with combat task pings. All combat task management is now done in player netty threads.

Bukkit:
- Disable paper plugin remapping
- Build a "lite" version that doesn't shade PacketEvents, allows multiple plugins to share PacketEvents or for you to update PacketEvents without rebuilding KnockbackSync

Fabric:
- Actually call onLoad() to init PE
- Explicitly prevent loading on Client
2025-03-14 23:19:13 -04:00
Axionize bf609a1964 Fix rare NPE 2025-03-03 13:08:58 -05:00
caseload f68d280821 Merge pull request #34 from Axionize/dev
- NPE and ping patches
2025-01-19 21:45:24 -06:00
caseload 5e7dcc4a42 Update README.md 2025-01-05 21:17:16 -06:00
Axionize af5446f33e - NPE and ping patches
- Fix NPE when toggling for player who is in combat
  - Fix packet queue filling up from unprocessed pings (sending pings to fast for client to process)
2025-01-05 05:47:27 -05:00
caseload 1f601f94ee Merge pull request #33 from Axionize/main
Main
2025-01-04 16:19:03 -06:00
Axionize da3ceeca55 Fix NPE caused from loading new PingSTrategy 2024-12-17 01:25:52 -05:00
Axionize f665d1e13e Fix NPE on startup when players join due to event UUID being null; introduced by new folia support 2024-12-17 00:30:22 -05:00
Axionize 8e52dae5b2 Pump PacketEvents to better support 1.21.4 2024-12-16 21:20:09 -05:00
Axionize 01188bc79f Only handly the packet type (KeepAlive/Transaction) our ping strategy cares about 2024-12-16 16:46:17 -05:00
Axionize 664d3899d7 Use CopyOnWriteArrayList() for thread safety on Folia 2024-12-16 14:37:17 -05:00
Axionize 60f0b37adc Fix CME when modifing listenerMap from different threads 2024-12-15 13:22:38 -05:00
Axionize 511c634c41 Attempt #3 to fix getBlockStateAt() in Folia 2024-12-14 14:59:29 -05:00
Axionize a3b5999de3 Revert "Fix more Folia compat issues"
This reverts commit 1beedd84
2024-12-14 14:14:02 -05:00
Axionize 1666d815b2 Revert "use Folia world on Folia servers"
This reverts commit 6d1ab1acbe.
2024-12-14 14:13:39 -05:00
Axionize b447154d52 Revert "Full Folia async support"
This reverts commit 73e862a782.
2024-12-14 14:13:38 -05:00
Axionize 73e862a782 Full Folia async support 2024-11-30 16:06:10 -05:00
Axionize 6d1ab1acbe use Folia world on Folia servers 2024-11-30 12:58:55 -05:00
Axionize 151eb58fb6 Fix a mistake in getBlockStateAt() 2024-11-30 12:51:23 -05:00
Axionize 1beedd84d0 Fix more Folia compat issues 2024-11-30 12:50:49 -05:00
Axionize bc3175faed Once again fix IllegalStateException packetSentList/packet conflict error 2024-11-30 02:55:26 -05:00
Axionize 58f61073c6 Fix reloading on Folia 2024-11-29 20:25:15 -05:00
Axionize 26c72b5490 Make updateInterval() improvement only work on Bukkit 2024-11-29 20:12:42 -05:00
Axionize 991e70e676 Fix Folia compatability 2024-11-29 19:58:39 -05:00
Axionize c8519f4d74 Bump fabric tools versions; make debugging easier 2024-11-29 18:41:51 -05:00
Axionize c9fef78eac Bump packetevents for 1.21.2/1.21.3 support 2024-11-22 13:01:58 -05:00
Axionize 0ce7104f72 Merge remote-tracking branch 'origin/dev' into dev 2024-11-22 12:58:31 -05:00
CASELOAD7000 74e7c3930c Add platform specific downloads, damage tick check 2024-11-07 18:26:16 -06:00
CASELOAD7000 31ab0aa7ff Remove debug again... 2024-11-06 23:28:16 -06:00
CASELOAD7000 fc34b3402e Add changes to auto updater 2024-11-06 23:27:39 -06:00
Axionize db6440d87c Fix ping priority and not checking for cancelled packets on send 2024-11-06 23:22:13 -05:00
CASELOAD7000 0119309ffb Remove debug 2024-11-06 03:22:21 -06:00
CASELOAD7000 f31fcc437f Add unfinished auto-updater 2024-11-06 03:18:27 -06:00
Axionize ab4d9d13c9 Code Cleanup 2024-11-03 22:09:25 -05:00
Axionize 052454fee0 Enable relocation to prevent library conflicts 2024-11-03 21:33:48 -05:00
Axionize 0f0b9f2cec Split up Ping send and Recieve listeners; Add priorities to listeners to be first to send/receive packets 2024-11-03 21:28:52 -05:00
Axionize 2eb1fff556 Remove loom from common sources 2024-11-01 23:52:49 -04:00
Axionize b4f8786683 Fix IndexOutofBoundsException that may occur in handler for ping packets. It has very limited impact on plugin functionality besides potentailly affecting ping measurements 2024-10-31 22:13:31 -04:00
Axionize 5c7f314071 Tix offground status 2024-10-29 16:25:19 -04:00
Axionize 73ab2388dc Remove building packetevents from actions script 2024-10-27 05:38:39 -04:00
Axionize 7c129185c5 Fix plugin loading on bukkit after refactor 2024-10-26 22:48:25 -04:00
Axionize ac5bd598f0 Bump gradle version in github actions to min required for packetevents-fabric 2.6.0 2024-10-26 22:32:47 -04:00
Axionize 98f23966db Fix clientBrand for fabricPlayer 2024-10-26 22:27:57 -04:00
Axionize 0803d2800f Remove redundant code 2024-10-26 22:26:34 -04:00
Axionize 83dab3cb8c Merge branch 'booky-packets' into dev 2024-10-26 22:25:45 -04:00
Axionize 768bc20e14 Packetevents 2.6.0 fabric fixes 2024-10-26 22:25:28 -04:00
Axionize f3bd42ff2f Add new ClientBrands pie 2024-10-26 22:14:17 -04:00
Axionize a56fd79d29 Initial work to port to 2.6.0 packetevents-fabric 2024-10-26 21:24:00 -04:00
Axionize e1b2487396 Try booky10 fabric fail 2024-10-26 18:34:00 -04:00
Axionize f19499205f Add showing offground sync status 2024-10-26 18:05:06 -04:00
Axionize 39e8cbddcb Config fixes - Fix reloading toggleoffground 2024-10-26 17:45:21 -04:00
Axionize 42c3129060 Merge remote-tracking branch 'origin/workspace' into dev-upstream
# Conflicts:
#	common/src/main/java/me/caseload/knockbacksync/listener/PlayerKnockbackListener.java
#	common/src/main/java/me/caseload/knockbacksync/player/PlayerData.java
#	common/src/main/java/me/caseload/knockbacksync/util/ChatUtil.java
#	common/src/main/java/me/caseload/knockbacksync/util/MathUtil.java
2024-10-26 17:27:20 -04:00
Axionize a6a2577e07 Config fixes 2024-10-26 17:11:19 -04:00
CASELOAD7000 47ff08d62a Revert to getHitBlockPosition() 2024-10-26 16:09:08 -05:00
Axionize 619e50dd6a Revert to getHitBlockPosition() 2024-10-26 17:08:53 -04:00
Axionize 98cbf4adfc Make every message besides root configureable 2024-10-26 17:00:59 -04:00
CASELOAD7000 185bb72f10 Reduce redundancy and fix block check 2024-10-26 15:21:10 -05:00
Axionize 7b5949dba6 /tick rate support on Bukkit 2024-10-26 15:40:18 -04:00
Axionize 0be787c1e2 Make all base variables consistently encapsulated 2024-10-26 15:08:08 -04:00
Axionize 74ee5d8c7e Initial offground synchronization commit 2024-10-26 14:40:40 -04:00
Axionize 2d98d4cbff Compensated ping message formatting 2024-10-26 14:40:38 -04:00
Axionize c2bacd2f5c Improve accuracy of getDistanceToGround() 2024-10-26 12:47:04 -04:00
Axionize 164d073861 Merge branch 'dev-upstream' into dev 2024-10-26 05:08:54 -04:00
Axionize 77c4dde70f Merge branch 'workspace' into dev-upstream 2024-10-26 05:08:36 -04:00
Axionize 3745648821 Fix config reloading 2024-10-26 05:07:53 -04:00
Axionize b339a53dc8 Fix config reloading 2024-10-26 04:39:35 -04:00
Axionize 3895b35941 Merge branch 'dev-upstream' into dev 2024-10-26 04:32:18 -04:00
Axionize ac5175b63c Decrease keepalive interval and autoupdate config (#27) 2024-10-26 04:31:45 -04:00
Axionize 8e61e44012 Merge branch 'dev-upstream' into dev 2024-10-26 03:46:05 -04:00
Axionize 233a1e04d3 Fix showing wrong ping and jitter after toggling knockbacksync off and on (#26) 2024-10-26 03:44:34 -04:00
Axionize 170caff375 fix showing wrong pinger and jitter after toggling off and then on 2024-10-26 03:41:29 -04:00
Axionize a2cc42a0a7 Filter out null trackedEntitiy 2024-10-26 03:03:27 -04:00
Axionize ae76933a5d PacketEvents, Tick Rate, and Ping Command Info Changes
- Account for /tick rate properly on Fabric once again, can be modified in the future to account for /tick rate on bukkit
- Switch back to using my PacketEvents fork for Fabric
- Add more information to the ping command including if ping has spiked or not
2024-10-26 02:57:05 -04:00
CASELOAD7000 992ab4002a Switch PacketEvents build 2024-10-26 01:09:00 -05:00
CASELOAD7000 bb5b39810c Account for /tick rate 2024-10-26 01:07:01 -05:00
CASELOAD7000 dc7edf4361 Add more information to ping command 2024-10-26 01:01:30 -05:00
Axionize 702618482d Minor code cleanup, use only Java 8 features 2024-10-26 01:02:51 -04:00
Axionize a35b208df2 Merge branch 'dev' into dev-upstream
# Conflicts:
#	bukkit/src/main/java/me/caseload/knockbacksync/BukkitBase.java
2024-10-26 00:43:46 -04:00
Axionize a51e9ac32e Fix player death causing console spam with new updateInterval() 2024-10-26 00:39:57 -04:00
Axionize f4df09802b Delete unused UpdateIntervalListener 2024-10-26 00:21:07 -04:00
CASELOAD7000 0a08b76d35 Change default permissions 2024-10-25 23:14:11 -05:00
CASELOAD7000 f7e572ef02 Rename getJumpVelocity to getJumpPower 2024-10-25 21:30:30 -05:00
CASELOAD7000 40d60b62a2 Improve prediction accuracy 2024-10-25 20:18:04 -05:00
CASELOAD7000 92f0cf7729 Improve prediction accuracy 2024-10-25 20:10:57 -05:00
CASELOAD7000 b34417d1c8 Remove PlayerJumpListener 2024-10-25 19:21:23 -05:00
Axionize b9cc9f1f7f Merge branch 'dev' into dev-upstream 2024-10-25 16:47:31 -04:00
Axionize 0c7b6effde Add support for custom updateInterval() for players on Bukkit/Spigot/Paper + Folia > 1.20.5 2024-10-25 16:47:11 -04:00
Axionize f2807153b5 Fix README 2024-10-25 15:00:50 -04:00
Axionize 4a460f28cd Update developer notes 2024-10-25 14:59:23 -04:00
Axionize fb62b8d99d Remove maven build file 2024-10-25 14:55:25 -04:00
Axionize e056cec07c Custom events and reloading now fully work 2024-10-25 14:18:12 -04:00
Axionize e18a58e4f2 Make config edits from in-game preserve comments 2024-10-25 13:37:55 -04:00
Axionize 36a2bb5165 Fabric crash hotfix 2024-10-25 10:05:06 -04:00
Axionize 7a898acfd9 Update buildscript to use repo variables 2024-10-25 09:17:15 -04:00
Axionize ac5d3307ed Gradle cache builds 2024-10-24 21:40:58 -04:00
Axionize a652147e2c Make buildscript use variables for Modrinth ID and repository URL; Make hash provider gitihub URL build-time configurable 2024-10-24 21:38:02 -04:00
Axionize f7c4e179b7 Fix plugin.yml 2024-10-24 21:15:03 -04:00
Axionize 3991e8f09a Add missing mixin on Fabric 2024-10-24 21:02:33 -04:00
Axionize 44239355b9 Fabric build fix 2024-10-24 20:58:18 -04:00
Axionize 0b6af2aacd Minor refactor 2024-10-24 19:44:20 -04:00
Axionize 85c6a2cf14 Implement updateInterval() modification and getJumpVelocity() on Fabric 2024-10-24 19:43:53 -04:00
Axionize ca937a86d0 Merge branch 'dev' into dev-upstream
# Conflicts:
#	bukkit/src/main/java/me/caseload/knockbacksync/player/BukkitPlayer.java
#	common/src/main/java/me/caseload/knockbacksync/command/subcommand/PingCommand.java
#	common/src/main/java/me/caseload/knockbacksync/command/subcommand/ReloadCommand.java
#	common/src/main/java/me/caseload/knockbacksync/command/subcommand/StatusCommand.java
#	common/src/main/java/me/caseload/knockbacksync/player/PlatformPlayer.java
#	fabric/src/main/java/me/caseload/knockbacksync/player/FabricPlayer.java
2024-10-24 17:02:45 -04:00
Axionize 8a2f85fadc Finally fix wrong KB being applied bug 2024-10-24 16:31:27 -04:00
Axionize 24b32f2eca Fix Conversion from Fabric to Packetevents BlockStates 2024-10-24 15:51:41 -04:00
Axionize 7bfe5c584f Fix Conversion from Bukkit to Packetevents BlockState conversion for Spigot 2024-10-24 15:44:29 -04:00
Axionize e904f44a5f Fix new custom updateInterval for players 2024-10-24 15:10:16 -04:00
Axionize 9797780d81 Use the more efficient Vector3D over Location for bounding box calculations 2024-10-24 14:29:16 -04:00
Axionize 9da919e910 Make BukkitPlayerDamageListener use early return paradigm like the cross-platform code 2024-10-24 14:15:15 -04:00
Axionize fe395f2cbd Revert to using getHitBlockPosition() instead of getHitPosition() 2024-10-22 06:56:01 -04:00
CASELOAD7000 0e39ea620b Add unused off ground patch 2024-10-22 05:37:37 -05:00
Axionize 581bef78fe Change packet ids 2024-10-22 06:11:54 -04:00
Axionize 35e19c56b1 Fix Bounding box calculation logic to use old algorithm 2024-10-22 06:09:44 -04:00
CASELOAD7000 e2514fd229 Rename classes 2024-10-21 21:41:37 -05:00
CASELOAD7000 1fceee5319 Remove var usage 2024-10-21 21:32:29 -05:00
CASELOAD7000 07b34d4744 Refactor getPlatform 2024-10-21 21:27:22 -05:00
CASELOAD7000 b8ac667e5f Another test 2024-10-21 21:07:52 -05:00
BuildTools a467a06df4 test 2024-10-21 20:49:13 -05:00
Axionize 50bde1375a First attempt at Custom entity tick intervals 2024-10-21 19:53:11 -04:00
Axionize 6ce4469db9 Add event system, refactor some configs to use, tood rest later 2024-10-21 18:09:20 -04:00
Axionize d63e4a0c29 Mark fabric dev builds as alpha instead of beta automatically 2024-10-21 16:46:39 -04:00
Axionize e5b70523bc More robust logging when reflection fails 2024-10-21 16:44:08 -04:00
Axionize cf0e5decd7 Attempt to fix flying KB introduced in b45 2024-10-21 16:32:39 -04:00
Axionize cbe1c3c46e Fix gradle wrapper support 2024-10-20 23:21:08 -04:00
Axionize 1db1e02c8b Fix support for Paper 1.20.6+ 2024-10-20 03:59:51 -04:00
Axionize 7a9fee6e2d 1.12.2 backport 2024-10-20 01:50:48 -04:00
99 changed files with 2751 additions and 1518 deletions

No files matched your search

+69 -82
View File
@@ -19,39 +19,20 @@ jobs:
- name: Checkout main repository
uses: actions/checkout@v2
# 2. Install JDK 21 – NO Gradle cache here
- name: Set up JDK
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'adopt'
distribution: temurin
java-version: 21
cache: '' # ← disable Maven/Gradle cache
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
# 3. Set up Gradle + dependency cache
- name: Set up Gradle 8.13
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.8
# Temporary workaround to packetevents bugs, use custom fork build
- name: Checkout packetevents repository
uses: actions/checkout@v3
with:
repository: Axionize/packetevents
ref: 2.0-fabric-serverside-fix
path: packetevents
- name: Update version in packetevents
run: |
cd packetevents
sed -i 's/val fullVersion = ".*"/val fullVersion = "2.5.8"/' build.gradle.kts
- name: Build packetevents with Gradle
run: |
cd packetevents
gradle build -x test
- name: Publish packetevents to Maven Local
run: |
cd packetevents
gradle publishToMavenLocal
gradle-version: "8.13" # keep the quotes
# cache-enabled: true # default, uploads ~/.gradle
- name: Generate build number
id: buildnumber
@@ -59,6 +40,19 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Temporary tasks so built jar works because PacketEvents won't merge my fixes for their broken fabric support
- name: Clone Axionize's PacketEvents Repository
run: |
git clone -b fix/fabric-events https://github.com/Axionize/packetevents.git packetevents
cd packetevents
echo "Cloned PacketEvents repository"
- name: Build and Publish PacketEvents to Maven Local
uses: gradle/actions/setup-gradle@v3
with:
build-root-directory: packetevents
arguments: publishToMavenLocal
- name: Update version in build.gradle.kts
id: update_version
run: |
@@ -75,37 +69,26 @@ jobs:
# Ensure snapshot is set to false
sed -i "s/val snapshot = .*/val snapshot = false/" build.gradle.kts
- name: Build with Gradle
run: gradle clean build -x test
- name: Build without PE shaded
run: gradle build -PshadePE=false -x test
env:
GITHUB_REPOSITORY: ${{ github.repository }}
- name: Rename shaded jar to overwrite non-shaded jar
run: mv bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}-all.jar bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar
- name: Build with PE shaded Gradle
run: gradle build -PshadePE=true -x test
env:
GITHUB_REPOSITORY: ${{ github.repository }}
# Download PaperSpigot (Choose version for reproducibility
- name: Download PaperSpigot
run: |
mkdir -p paper-server/plugins
curl -o paperclip.jar https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/120/downloads/paper-1.21.1-120.jar
mv paperclip.jar paper-server/
# Copy Plugin into Plugins Folder and Start Server
- name: Copy Plugin and Start Paper Server
run: |
cp bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar paper-server/plugins/
cd paper-server
# Start the server to remap the plugin
timeout 60 java -Xms3G -Xmx3G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -jar -Dcom.mojang.eula.agree=true paperclip.jar --nogui || true
# Get the Hashes of the Original and Remapped JARs
# Get the Hashes of the Original and Lite JARs
- name: Calculate Hashes
run: |
# Calculate hash for the original JAR
ORIGINAL_HASH=$(sha256sum bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar | awk '{print $1}')
echo "ORIGINAL_HASH=$ORIGINAL_HASH" >> $GITHUB_ENV
# Calculate hash for the remapped JAR
REMAPPED_HASH=$(sha256sum paper-server/plugins/.paper-remapped/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar | awk '{print $1}')
echo "REMAPPED_HASH=$REMAPPED_HASH" >> $GITHUB_ENV
# Calculate hash for the remapped 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
@@ -115,7 +98,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Download the current dev-builds.txt
curl -L -o dev-builds.txt https://github.com/CASELOAD7000/knockback-sync/releases/latest/download/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
@@ -131,8 +114,8 @@ jobs:
}
# Add new hashes if they don't exist
add_hash_if_new "${{ env.ORIGINAL_HASH }}"
add_hash_if_new "${{ env.REMAPPED_HASH }}"
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
@@ -142,55 +125,58 @@ jobs:
echo "Changes detected in dev-builds.txt, uploading new version."
# Get the latest release ID
RELEASE_ID=$(curl -s "https://api.github.com/repos/CASELOAD7000/knockback-sync/releases/latest" | jq -r .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/CASELOAD7000/knockback-sync/releases/$RELEASE_ID/assets" | \
"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/CASELOAD7000/knockback-sync/releases/assets/$ASSET_ID"
"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/CASELOAD7000/knockback-sync/releases/$RELEASE_ID/assets?name=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: Create artifacts directory
run: mkdir -p artifacts
- name: Copy bukkit artifact
run: cp bukkit/build/libs/knockbacksync-bukkit-${{ env.NEW_VERSION }}.jar artifacts/
- name: Copy fabric artifact
run: cp fabric/build/libs/knockbacksync-fabric-${{ env.NEW_VERSION }}.jar artifacts/
- name: Zip the artifacts
run: zip -r KnockbackSync-${{ env.NEW_VERSION }}.zip artifacts/*
- name: Upload artifacts zip
- name: Upload Bukkit Artifact
uses: actions/upload-artifact@v4
with:
name: KnockbackSync-${{ env.NEW_VERSION }}.zip
path: KnockbackSync-${{ env.NEW_VERSION }}.zip
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: wGTbjSTq
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 }}
@@ -204,7 +190,7 @@ jobs:
purpur
game-versions: |
>=1.14.4
>=1.12.2
retry-attempts: 2
retry-delay: 10000
@@ -212,7 +198,7 @@ jobs:
- uses: Kir-Antipov/mc-publish@v3.3
with:
modrinth-id: wGTbjSTq
modrinth-id: ${{ vars.MODRINTH_ID }}
modrinth-featured: true
modrinth-unfeature-mode: subset
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
@@ -222,13 +208,14 @@ jobs:
name: KnockbackSync-${{ env.NEW_VERSION }}
version: ${{ env.NEW_VERSION }}
version-type: beta
version-type: alpha
loaders: |
fabric
game-versions: |
>=1.21
1.21.4
1.21.5
retry-attempts: 2
retry-delay: 10000
-1
View File
@@ -9,7 +9,6 @@ target/
.gradle/
gradle/
build/
run/
+5 -9
View File
@@ -1,4 +1,4 @@
Project Structure
## Project Structure
- `common` submodule contains shared code for all platforms.
- It contains the interfaces and abstractions for
@@ -6,11 +6,11 @@ Project Structure
- Scheduling
- Players
- Blocks
- World
- Worlds/Levels
- The Server
- In addition to the interfaces, the code also contains the implementations for
- Commands
- Commands use Brigadier with Reflection to handle version differences. Simply call our command code in common by registering KnockBackSyncCommand.build() and implementing CommandOperations
- 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)
@@ -21,6 +21,7 @@ If you want to add support for a new platform, simply make a new submodule and i
- 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
@@ -31,9 +32,4 @@ If you find your platform is lacking events, take a look at how the PlayerVeloci
## 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
- Command System
- context.getSource().sendSuccess() takes a Supplier<Component> in > 1.19.4
- context.getSource().sendSuccess() takes a Component in <= 1.19.4
- Component.literal() does not exist in <= 1.18.2
- Runnable and manual ping does not work on fabric right now due b/c packetevents. Ping measurement will be less accurate
+1 -17
View File
@@ -18,30 +18,16 @@ Official Discord: https://discord.gg/nnpqpAtyVW
### How does this change benefit high ping players?
**Knockback control.** For example, it will be easier to escape crit chains and punish crit.
<<<<<<< Updated upstream
### How do I change the ping offset?
Open up `config.yml` and edit the line that says `ping_offset: 25`. Replace 25 with your desired ping offset.
;
Then just type
```
/knockbacksync reload
```
or restart your server.
=======
### 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](common/src/main/java/me/caseload/knockbacksync/player/PlayerData.java) class.
>>>>>>> Stashed changes
## What servers are using this plugin?
| IP | Location | Region | Ping Offset | spike_threshold |
|------------------|-----------------------------------|--------|-------------|-----------------|
| `pvparcade.club` | Ashburn, Virginia, United States | NA | 20 | 15 |
| `stray.gg` | Ashburn, Virginia, United States | NA | 25 | 20 |
| `eu.stray.gg` | Frankfurt, Hesse, Germany | EU | 25 | 20 |
| `valed.gg` | Frankfurt, Hesse, Germany | EU | 25 | 20 |
| `eu.catpvp.xyz` | Frankfurt, Hesse, Germany | EU | 25 | 20 |
| `as.catpvp.xyz` | Singapore | AS | 25 | 20 |
@@ -171,11 +157,9 @@ This command reloads the KnockbackSync plugin's configuration file.
* Sends a message indicating whether the experimental off-ground knockback synchronization has been enabled or disabled. The messages are configurable in the `config.yml` file.
**Notes:**
* **This command currently does not do anything as off-ground synchronization has yet to be implemented.**
* This feature is experimental and may not work as expected.
## License
GNU General Public License v3.0 or later
See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.en.html) to see the full text.
See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.en.html) to see the full text.
+12 -1
View File
@@ -3,11 +3,20 @@ import java.io.ByteArrayOutputStream
plugins {
id("java")
id("com.gradleup.shadow") version "8.3.3" apply false
id("fabric-loom") version "1.7.4" apply false
id("fabric-loom") version "1.10.5" apply false
}
val fullVersion = "1.3.5"
val snapshot = true
val githubRepo = System.getenv("GITHUB_REPOSITORY") ?: project.findProperty("githubRepo").toString()
extra["shadePE"] = project.findProperty("shadePE")?.toString()?.toBoolean()
?: System.getenv("SHADE_PE")?.toBoolean()
?: true
extra["relocate"] = project.findProperty("relocate")?.toString()?.toBoolean()
?: System.getenv("RELOCATE_JAR")?.toBoolean()
?: true
allprojects {
fun getVersionMeta(includeHash: Boolean): String {
@@ -29,6 +38,7 @@ allprojects {
group = "me.caseload.knockbacksync"
version = "$fullVersion${getVersionMeta(true)}"
ext["versionNoHash"] = "$fullVersion${getVersionMeta(false)}"
ext["githubRepo"] = githubRepo
repositories {
mavenLocal()
@@ -42,6 +52,7 @@ allprojects {
}
maven("https://libraries.minecraft.net/")
maven("https://maven.neoforged.net/releases")
maven("https://repo.codemc.io/repository/maven-snapshots/")
}
}
+20 -6
View File
@@ -4,8 +4,10 @@ plugins {
id("com.gradleup.shadow")
}
val shadePE: Boolean by rootProject.extra
base {
archivesName.set("${rootProject.property("archives_base_name")}-bukkit")
archivesName.set("${rootProject.property("archives_base_name")}-bukkit${if (shadePE) "" else "-lite"}")
}
val shadeThisThing: Configuration by configurations.creating {
@@ -29,17 +31,19 @@ tasks.named<JavaCompile>("compileTestJava") {
dependencies {
implementation(project(":common"))
compileOnly("org.spigotmc:spigot-api:1.14.4-R0.1-SNAPSHOT")
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")!!)
shadeThisThing(implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")!!)
shadeThisThing(implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2")!!)
shadeThisThing(implementation("com.github.retrooper:packetevents-spigot:2.5.0")!!)
// shadeThisThing(implementation("net.kyori:adventure-platform-bukkit:4.3.4")!!)
if (shadePE) {
shadeThisThing(implementation("com.github.retrooper:packetevents-spigot:2.8.0-SNAPSHOT")!!)
} else {
compileOnly("com.github.retrooper:packetevents-spigot:2.8.0-SNAPSHOT")
}
shadeThisThing(implementation("org.incendo:cloud-paper:2.0.0-beta.10")!!)
shadeThisThing(implementation("org.incendo:cloud-core:2.0.0")!!)
@@ -48,6 +52,13 @@ dependencies {
}
tasks.withType<ShadowJar> {
manifest {
attributes["paperweight-mappings-namespace"] = "mojang"
}
// Remove the -all suffix from the output JAR file name
archiveClassifier.set("")
configurations = listOf(shadeThisThing)
isEnableRelocation = true
relocationPrefix = "${project.property("maven_group")}.${project.property("archives_base_name")}.shaded"
@@ -60,11 +71,14 @@ tasks.build {
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,
"depends" to if (shadePE) "[]" else listOf("packetevents")
)
}
}
@@ -0,0 +1,263 @@
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.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() {
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()
);
}
@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();
}
}
@@ -3,9 +3,9 @@ package me.caseload.knockbacksync;
import com.github.retrooper.packetevents.PacketEvents;
import org.bukkit.plugin.java.JavaPlugin;
public final class KBSyncBukkitLoaderPlugin extends JavaPlugin {
public final class BukkitLoaderPlugin extends JavaPlugin {
private final KnockbackSyncBase core = new KBSyncBukkitBase(this);
private final Base core = new BukkitBase(this);
@Override
public void onLoad() {
@@ -1,138 +0,0 @@
package me.caseload.knockbacksync;
import com.github.retrooper.packetevents.PacketEvents;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import me.caseload.knockbacksync.listener.bukkit.BukkitPlayerDamageListener;
import me.caseload.knockbacksync.listener.bukkit.BukkitPlayerJoinQuitListener;
import me.caseload.knockbacksync.listener.bukkit.BukkitPlayerKnockbackListener;
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.event.Listener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
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.util.logging.Logger;
public class KBSyncBukkitBase extends KnockbackSyncBase {
private final JavaPlugin plugin;
private final BukkitSenderFactory bukkitSenderFactory = new BukkitSenderFactory(this);
private final PluginPermissionChecker permissionChecker = new PluginPermissionChecker();
public KBSyncBukkitBase(JavaPlugin plugin) {
this.plugin = plugin;
super.configManager = new ConfigManager();
super.playerSelectorParser = new BukkitPlayerSelectorParser<>();
super.statsManager = new BukkitStatsManager();
super.platformServer = new BukkitServer();
super.pluginJarHashProvider = new PluginJarHashProvider(this.getClass().getProtectionDomain().getCodeSource().getLocation());
}
@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() {
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this.plugin));
PacketEvents.getAPI().load();
}
@Override
public void enable() {
super.enable();
initializeScheduler();
configManager.loadConfig(false);
statsManager.init();
checkForUpdates();
}
@Override
public void initializeScheduler() {
switch (platform) {
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 BukkitPlayerJoinQuitListener()
);
}
@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;
}
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;
}
}
@@ -15,9 +15,12 @@ public class BukkitPlayerDamageListener extends PlayerDamageListener implements
public void onPlayerDamage(EntityDamageByEntityEvent event) {
Entity victim = event.getEntity();
Entity attacker = event.getDamager();
if ((victim instanceof Player) && (attacker instanceof Player))
if (!MultiLibUtil.isExternalPlayer((Player) victim)) {
onPlayerDamage(new BukkitPlayer((Player) victim), new BukkitPlayer((Player) attacker));
}
if (!(victim instanceof Player) || !(attacker instanceof Player))
return;
if (MultiLibUtil.isExternalPlayer((Player) victim))
return;
onPlayerDamage(new BukkitPlayer((Player) victim), new BukkitPlayer((Player) attacker));
}
}
@@ -1,22 +0,0 @@
package me.caseload.knockbacksync.listener.bukkit;
import me.caseload.knockbacksync.listener.PlayerJoinQuitListener;
import me.caseload.knockbacksync.player.BukkitPlayer;
import me.caseload.knockbacksync.player.PlayerData;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class BukkitPlayerJoinQuitListener extends PlayerJoinQuitListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
onPlayerJoin(new PlayerData(new BukkitPlayer(event.getPlayer())));
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
onPlayerQuit(event.getPlayer().getUniqueId());
}
}
@@ -34,8 +34,7 @@ public class BukkitPlayerKnockbackListener extends PlayerKnockbackListener imple
if (MultiLibUtil.isExternalPlayer(victim))
return;
Vector vector = event.getVelocity();
Vector vector = victim.getVelocity();
onPlayerVelocity(new BukkitPlayer(victim), new Vector3d(vector.getX(), vector.getY(), vector.getZ()));
// callback.setVelocity(victim.getVelocity()); // Update the callback's velocity in Spigot
}
}
@@ -1,48 +1,83 @@
package me.caseload.knockbacksync.player;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.PacketEventsAPI;
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.Location;
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 {
String bukkitversion = Bukkit.getServer().getClass().getPackage()
.getName();
// Step 1: Load the CraftPlayer class
craftPlayerClass = Class.forName(bukkitversion + ".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("net.minecraft.server." + bukkitversion + ".EntityPlayer");
getAttackStrengthScaleMethod = entityPlayerClass.getDeclaredMethod("getAttackStrengthScale", float.class);
getAttackStrengthScaleMethod.setAccessible(true);
// 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) {
e.printStackTrace();
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
@@ -93,7 +128,11 @@ public class BukkitPlayer implements PlatformPlayer {
@Override
public int getPing() {
return PacketEvents.getAPI().getPlayerManager().getPing(bukkitPlayer);
if (currentVersion.isNewerThanOrEquals(ServerVersion.V_1_16_5)) {
return bukkitPlayer.getPing();
} else {
return PacketEvents.getAPI().getPlayerManager().getPing(bukkitPlayer);
}
}
@Override
@@ -103,12 +142,12 @@ public class BukkitPlayer implements PlatformPlayer {
@Override
public PlatformWorld getWorld() {
return new SpigotWorld(bukkitPlayer.getWorld());
return BukkitBase.INSTANCE.getPlatform() == Platform.FOLIA ? new FoliaWorld(bukkitPlayer.getWorld()): new SpigotWorld(bukkitPlayer.getWorld());
}
@Override
public Vector3d getLocation() {
Location location = bukkitPlayer.getLocation();
org.bukkit.Location location = bukkitPlayer.getLocation();
return new Vector3d(location.getX(), location.getY(), location.getZ());
}
@@ -119,16 +158,18 @@ public class BukkitPlayer implements PlatformPlayer {
@Override
public double getAttackCooldown() {
try {
// Step 1: Get the CraftPlayer instance
Object craftPlayer = craftPlayerClass.cast(bukkitPlayer);
// Step 2: Get the handle (NMS EntityPlayer)
Object entityPlayer = getHandleMethod.invoke(craftPlayer);
// Step 3: Invoke the getAttackStrengthScale method
return (float) getAttackStrengthScaleMethod.invoke(entityPlayer, 0.5f);
} catch (Exception e) {
e.printStackTrace();
return -1; // Return -1 if an error occurs
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!");
}
}
}
@@ -151,4 +192,44 @@ public class BukkitPlayer implements PlatformPlayer {
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;
}
}
@@ -1,55 +1,46 @@
package me.caseload.knockbacksync.scheduler;
import io.github.retrooper.packetevents.util.folia.FoliaScheduler;
import io.github.retrooper.packetevents.util.folia.GlobalRegionScheduler;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
public class FoliaSchedulerAdapter implements SchedulerAdapter {
private final Plugin plugin;
private GlobalRegionScheduler scheduler = null;
public FoliaSchedulerAdapter(Plugin plugin) {
this.plugin = plugin;
try {
// Attempt to find and call the `getGlobalRegionScheduler` method
Method getSchedulerMethod = Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler");
scheduler = (GlobalRegionScheduler) getSchedulerMethod.invoke(Bukkit.getServer());
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
plugin.getLogger().severe("Failed to access GlobalRegionScheduler: " + e.getMessage());
}
}
@Override
public AbstractTaskHandle runTask(Runnable task) {
return new FoliaTaskHandle(scheduler.run(plugin, scheduledTask -> task.run()));
return new FoliaTaskHandle(FoliaScheduler.getGlobalRegionScheduler().run(plugin, scheduledTask -> task.run()));
}
@Override
public AbstractTaskHandle runTaskAsynchronously(Runnable task) {
return new FoliaTaskHandle(scheduler.run(plugin, scheduledTask -> task.run()));
return new FoliaTaskHandle(FoliaScheduler.getAsyncScheduler().runNow(plugin, scheduledTask -> task.run()));
}
@Override
public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
return new FoliaTaskHandle(scheduler.runDelayed(plugin, scheduledTask -> task.run(), 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(scheduler.runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks));
return new FoliaTaskHandle(FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks));
}
@Override
public AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay) {
return new FoliaTaskHandle(scheduler.runDelayed(plugin, scheduledTask -> task.run(), 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(scheduler.runAtFixedRate(plugin, scheduledTask -> task.run(), delay, period));
return new FoliaTaskHandle(FoliaScheduler.getAsyncScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delay, period));
}
// Folia takes care of this
@@ -1,41 +1,19 @@
package me.caseload.knockbacksync.scheduler;
import io.github.retrooper.packetevents.util.folia.TaskWrapper;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class FoliaTaskHandle implements AbstractTaskHandle {
private static final MethodHandle GET_OWNING_PLUGIN;
private static final MethodHandle CANCEL;
static {
try {
Class<?> scheduledTaskClass = Class.forName("io.papermc.paper.threadedregions.scheduler.ScheduledTask");
MethodHandles.Lookup lookup = MethodHandles.lookup();
GET_OWNING_PLUGIN = lookup.findVirtual(scheduledTaskClass, "getOwningPlugin", MethodType.methodType(Plugin.class));
CANCEL = lookup.findVirtual(scheduledTaskClass, "cancel", MethodType.methodType(void.class));
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException("Failed to initialize FoliaTaskHandle reflection", e);
}
}
private final Object scheduledTask; // Store as Object instead of ScheduledTask
private final TaskWrapper scheduledTask; // Store as Object instead of ScheduledTask
private boolean cancelled;
public FoliaTaskHandle(@NotNull Object scheduledTask) {
public FoliaTaskHandle(@NotNull TaskWrapper scheduledTask) {
this.scheduledTask = scheduledTask;
}
public Plugin getOwner() {
try {
return (Plugin) GET_OWNING_PLUGIN.invoke(scheduledTask);
} catch (Throwable e) {
throw new RuntimeException("Failed to invoke getOwningPlugin", e);
}
return scheduledTask.getOwner();
}
@Override
@@ -45,11 +23,7 @@ public class FoliaTaskHandle implements AbstractTaskHandle {
@Override
public void cancel() {
try {
CANCEL.invoke(scheduledTask);
} catch (Throwable e) {
throw new RuntimeException("Failed to invoke cancel", e);
}
scheduledTask.cancel();
this.cancelled = true;
}
}
@@ -1,6 +1,6 @@
package me.caseload.knockbacksync.sender;
import me.caseload.knockbacksync.KBSyncBukkitBase;
import me.caseload.knockbacksync.BukkitBase;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.RemoteConsoleCommandSender;
@@ -12,10 +12,10 @@ import org.incendo.cloud.SenderMapper;
import java.util.UUID;
public class BukkitSenderFactory extends SenderFactory<KBSyncBukkitBase, CommandSender> implements SenderMapper<CommandSender, Sender> {
public class BukkitSenderFactory extends SenderFactory<BukkitBase, CommandSender> implements SenderMapper<CommandSender, Sender> {
// private final BukkitAudiences audiences;
public BukkitSenderFactory(KBSyncBukkitBase kbSyncBukkitBase) {
public BukkitSenderFactory(BukkitBase kbSyncBukkitBase) {
super(kbSyncBukkitBase);
// this.audiences = BukkitAudiences.create(kbSyncBukkitBase.getLoader());
}
@@ -1,17 +1,11 @@
package me.caseload.knockbacksync.stats.custom;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import org.bukkit.plugin.Plugin;
public class BukkitStatsManager extends StatsManager {
@Override
public void init() {
KnockbackSyncBase.INSTANCE.getScheduler().runTaskAsynchronously(() -> {
BuildTypePie.determineBuildType(); // Function to calculate hash
MetricsBukkit metrics = new MetricsBukkit(23568);
metrics.addCustomChart(new PlayerVersionsPie());
metrics.addCustomChart(new BuildTypePie());
super.metrics = metrics;
});
public BukkitStatsManager(Plugin plugin) {
super(new MetricsBukkit(plugin, 23568));
}
}
@@ -15,8 +15,8 @@ package me.caseload.knockbacksync.stats.custom;
* Violations will result in a ban of your plugin and account from bStats.
*/
import me.caseload.knockbacksync.KBSyncBukkitLoaderPlugin;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -34,12 +34,11 @@ import java.util.logging.Level;
public class MetricsBukkit implements Metrics {
private final Plugin plugin;
private final MetricsBase metricsBase;
public MetricsBukkit(int serviceId) {
this.plugin = KBSyncBukkitLoaderPlugin.getPlugin(KBSyncBukkitLoaderPlugin.class);
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");
@@ -91,7 +90,7 @@ public class MetricsBukkit implements Metrics {
enabled,
this::appendPlatformData,
this::appendServiceData,
submitDataTask -> KnockbackSyncBase.INSTANCE.getScheduler().runTask(submitDataTask),
submitDataTask -> Base.INSTANCE.getScheduler().runTask(submitDataTask),
// isFolia
// ? null
// : submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
@@ -3,6 +3,7 @@ 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;
@@ -19,4 +20,9 @@ public class BukkitServer implements PlatformServer {
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());
}
}
@@ -1,18 +1,58 @@
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 {
private final World world;
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;
@@ -40,32 +80,90 @@ public class SpigotWorld implements PlatformWorld {
@Override
public WrappedBlockState getBlockStateAt(int x, int y, int z) {
Block block = world.getBlockAt(x, y, z);
return WrappedBlockState.getByString(block.getType().name());
return SpigotConversionUtil.fromBukkitBlockData(block.getBlockData());
}
@Override
public WrappedBlockState getBlockStateAt(Vector3d loc) {
return getBlockStateAt((int) Math.floor(loc.x), (int) Math.floor(loc.x), (int) Math.floor(loc.x));
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) {
Vector startVec = new Vector(start.getX(), start.getY(), start.getZ());
Vector directionVec = new Vector(direction.getX(), direction.getY(), direction.getZ());
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;
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);
org.bukkit.util.RayTraceResult result = world.rayTraceBlocks(startVec.toLocation(world), directionVec, maxDistance, fluidMode, ignorePassableBlocks);
if (result == null) return null;
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 ? WrappedBlockState.getByString(result.getHitBlock().getType().name()) : 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);
}
}
+8 -3
View File
@@ -1,10 +1,11 @@
name: KnockbackSync
version: ${version}
main: me.caseload.knockbacksync.KBSyncBukkitLoaderPlugin
api-version: '1.14'
authors: [ Caseload ]
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
depends: ${depends}
permissions:
knockbacksync.update:
default: op
@@ -15,4 +16,8 @@ permissions:
knockbacksync.toggle.self:
default: op
knockbacksync.ping:
default: op
knockbacksync.reload:
default: op
knockbacksync.toggleoffground:
default: op
+13 -17
View File
@@ -1,24 +1,24 @@
plugins {
id("fabric-loom")
// id("net.neoforged.moddev") version "1.0.11"
id("com.github.gmazzo.buildconfig") version "3.1.0"
}
dependencies {
minecraft("com.mojang:minecraft:1.16.5")
mappings(loom.layered {
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-1.16.5:2022.03.06")
})
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")
// Shaded in or bundled by platform-specific code
implementation("com.github.retrooper:packetevents-api:2.5.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2")
if (shadePE) {
implementation("com.github.retrooper:packetevents-api:2.8.0-SNAPSHOT")
} else {
compileOnly("com.github.retrooper:packetevents-api:2.8.0-SNAPSHOT")
}
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")
@@ -28,10 +28,6 @@ dependencies {
implementation("org.incendo:cloud-minecraft-extras:2.0.0-beta.10")
}
repositories {
maven("https://maven.neoforged.net/releases")
}
tasks.named("remapJar").configure {
enabled = false
buildConfig {
buildConfigField("String", "GITHUB_REPO", "\"${project.rootProject.ext["githubRepo"]}\"")
}
@@ -9,65 +9,76 @@ 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.listener.packetevents.AttributeChangeListener;
import me.caseload.knockbacksync.listener.packetevents.PingReceiveListener;
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.util.Arrays;
import java.util.List;
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 KnockbackSyncBase {
public abstract class Base {
public static Logger LOGGER;
public static KnockbackSyncBase INSTANCE;
public static Base INSTANCE;
public final Platform platform;
public StatsManager statsManager;
public PlatformServer platformServer;
@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;
protected CommandManager<Sender> commandManager;
@Getter protected CommandManager<Sender> commandManager;
@Getter protected final EventBus eventBus = new OptimizedEventBus();
@Getter
protected AbstractPlayerSelectorParser<Sender> playerSelectorParser;
protected KnockbackSyncBase() {
this.platform = getPlatform();
protected Base() {
this.platform = detectPlatform();
INSTANCE = this;
}
private Platform getPlatform() {
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
return Platform.FOLIA; // Paper (Folia) detected
} catch (ClassNotFoundException ignored1) {
}
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);
}});
try {
Class.forName("org.bukkit.Bukkit");
return Platform.BUKKIT; // Bukkit (Spigot/Paper without Folia) detected
} catch (ClassNotFoundException ignored2) {
}
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("net.fabricmc.loader.api.FabricLoader");
return Platform.FABRIC; // Fabric detected
} catch (ClassNotFoundException ignored3) {
Class.forName(className);
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
throw new IllegalStateException("Unknown platform!");
}
public abstract Logger getLogger();
@@ -85,6 +96,10 @@ public abstract class KnockbackSyncBase {
registerCommonListeners();
registerPlatformListeners();
registerCommands();
initializeScheduler();
configManager.loadConfig(false);
statsManager.init();
checkForUpdates();
}
public abstract void initializeScheduler();
@@ -100,8 +115,12 @@ public abstract class KnockbackSyncBase {
protected void registerCommonListeners() {
PacketEvents.getAPI().getEventManager().registerListeners(
new AttributeChangeListener(),
new PingReceiveListener()
new PingSendListener(),
new PingReceiveListener(),
new PacketPlayerJoinQuit(),
new ClientBrandListener()
);
Event.setEventBus(eventBus);
}
protected abstract void registerPlatformListeners();
@@ -112,12 +131,12 @@ public abstract class KnockbackSyncBase {
new ReloadCommand(),
new PingCommand(),
new StatusCommand(),
new ToggleOffGroundSubcommand(),
new ToggleCommand()
);
list.forEach(command -> command.register(commandManager));
}
protected abstract String getVersion();
protected void checkForUpdates() {
@@ -138,15 +157,25 @@ public abstract class KnockbackSyncBase {
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")) {
if (currentVersion.contains("-dev"))
LOGGER.info("You are running a development build newer than the latest release.");
} else {
else {
LOGGER.info("You are running a version newer than the latest release.");
}
configManager.setUpdateAvailable(false);
} else {
LOGGER.info("You are running the latest release.");
configManager.setUpdateAvailable(false);
}
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());
@@ -154,6 +183,44 @@ public abstract class KnockbackSyncBase {
});
}
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("[-.]");
@@ -213,6 +280,13 @@ public abstract class KnockbackSyncBase {
public abstract PermissionChecker getPermissionChecker();
public abstract float getTickRate();
public abstract URL getJarURL();
public abstract void restartServer();
public abstract boolean hasRestartScript();
}
@@ -1,16 +1,40 @@
package me.caseload.knockbacksync.command.subcommand;
import me.caseload.knockbacksync.KnockbackSyncBase;
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")
@@ -23,20 +47,83 @@ public class PingCommand implements BuilderCommand {
return PredicatePermission.of(senderPredicate).testPermission(sender);
}))
.literal("ping")
.optional("target", KnockbackSyncBase.INSTANCE.getPlayerSelectorParser().descriptor())
.optional("target", Base.INSTANCE.getPlayerSelectorParser().descriptor())
.handler(context -> {
PlayerSelector targetSelector = context.getOrDefault("target", null);
if (targetSelector == null) {
if (context.sender().isConsole()) {
context.sender().sendMessage("You must specify a player to use this command from the console.");
context.sender().sendMessage(ChatUtil.translateAlternateColorCodes('&', mustSpecifyPlayerFromConsoleMessage));
} else {
context.sender().sendMessage(ChatUtil.getPingMessage(context.sender().getUniqueId(), null));
context.sender().sendMessage(getPingMessage(context.sender().getUniqueId(), context.sender().getName(), null, null));
}
} else {
context.sender().sendMessage(ChatUtil.getPingMessage(context.sender().getUniqueId(), targetSelector.getSinglePlayer().getUUID()));
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();
}
}
@@ -1,7 +1,9 @@
package me.caseload.knockbacksync.command.subcommand;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -11,27 +13,38 @@ 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);
};
.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 configManager = KnockbackSyncBase.INSTANCE.getConfigManager();
configManager.loadConfig(true);
return PredicatePermission.of(senderPredicate).testPermission(sender);
}))
.handler(context -> {
configManager.loadConfig(true);
String rawReloadMessage = configManager.getReloadMessage();
String reloadMessage = ChatUtil.translateAlternateColorCodes('&', rawReloadMessage);
// Fire the ConfigReloadEvent
new ConfigReloadEvent(configManager).post();
context.sender().sendMessage(reloadMessage);
})
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.");
}
}
@@ -1,8 +1,11 @@
package me.caseload.knockbacksync.command.subcommand;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -11,20 +14,33 @@ 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 StatusCommand implements BuilderCommand {
private static final ConfigManager configManager = KnockbackSyncBase.INSTANCE.getConfigManager();
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", KnockbackSyncBase.INSTANCE.getPlayerSelectorParser().descriptor())
.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);
@@ -35,53 +51,82 @@ public class StatusCommand implements BuilderCommand {
.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('&',
configManager.getConfigWrapper().getString("global_status_message", "&eGlobal KnockbackSync status: ")) +
(globalStatus
? ChatUtil.translateAlternateColorCodes('&', "&aEnabled")
: ChatUtil.translateAlternateColorCodes('&', "&cDisabled")));
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, KnockbackSyncBase.INSTANCE.platformServer.getPlayer(sender.getUniqueId()), configManager);
showPlayerStatus(sender, Base.INSTANCE.getPlatformServer().getPlayer(sender.getUniqueId()));
}
} else {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', "&cYou do not have permisssion to check your knockbacksync status."));
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noSelfPermissionMessage));
}
} else {
if (sender.hasPermission(STATUS_OTHER_PERMISSION)) {
if (sender.hasPermission(STATUS_OTHER_PERMISSION, true)) {
PlatformPlayer target = targetSelector.getSinglePlayer();
showPlayerStatus(sender, target, configManager);
showPlayerStatus(sender, target);
} else {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', "&cYou do not have permisssion to check the status of other players!"));
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noOtherPermissionMessage));
}
}
})
);
Base.INSTANCE.getEventBus().registerListeners(this);
}
private static void showPlayerStatus(Sender sender, PlatformPlayer target, ConfigManager configManager) {
boolean globalStatus = configManager.isToggled();
UUID uuid = target.getUUID();
boolean playerStatus = PlayerDataManager.containsPlayerData(uuid);
if (!globalStatus) {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&',
configManager.getConfigWrapper().getString("player_status_message", "&e%player%'s KnockbackSync status: ")
.replace("%player%", target.getName()) +
ChatUtil.translateAlternateColorCodes('&',
configManager.getConfigWrapper().getString("player_disabled_global_message", "&cDisabled (Global toggle is off)"))));
} else {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&',
configManager.getConfigWrapper().getString("player_status_message", "&e%player%'s KnockbackSync status: ")
.replace("%player%", target.getName()) +
(playerStatus
? ChatUtil.translateAlternateColorCodes('&', "&aEnabled")
: ChatUtil.translateAlternateColorCodes('&', "&cDisabled"))));
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!");
}
}
@@ -1,40 +1,61 @@
package me.caseload.knockbacksync.command.subcommand;
import me.caseload.knockbacksync.KnockbackSyncBase;
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.player.PlayerData;
import me.caseload.knockbacksync.sender.Sender;
import me.caseload.knockbacksync.util.ChatUtil;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.permission.Permission;
import org.incendo.cloud.permission.PredicatePermission;
import java.util.UUID;
import java.util.function.Predicate;
public class ToggleCommand implements BuilderCommand {
private static final ConfigManager configManager = KnockbackSyncBase.INSTANCE.getConfigManager();
private static final PermissionChecker permissionChecker = KnockbackSyncBase.INSTANCE.getPermissionChecker();
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", KnockbackSyncBase.INSTANCE.getPlayerSelectorParser().descriptor())
.optional("target", Base.INSTANCE.getPlayerSelectorParser().descriptor())
.permission((sender -> {
Predicate<Sender> senderPredicate = (s) -> {
return s.hasPermission(TOGGLE_GLOBAL_PERMISSION, false)
|| sender.hasPermission(TOGGLE_SELF_PERMISSION, true)
|| sender.hasPermission(TOGGLE_SELF_PERMISSION, false)
|| sender.hasPermission(TOGGLE_OTHER_PERMISSION, false);
};
@@ -46,25 +67,25 @@ public class ToggleCommand implements BuilderCommand {
if (targetSelector == null) {
// Global toggle
if (permissionChecker.hasPermission(sender, TOGGLE_GLOBAL_PERMISSION, false)) {
toggleGlobalKnockback(configManager, sender);
toggleGlobalKnockback(sender);
} else {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', "&cYou don't have permission to toggle the global setting."));
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("You do not have permission to toggle the knockback of other player's.");
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noOtherPermissionMessage));
return;
} else if (senderIsTarget && !permissionChecker.hasPermission(sender, TOGGLE_SELF_PERMISSION, true)) {
sender.sendMessage("You do not have permission to toggle your knockback.");
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', noSelfPermissionMessage));
return;
}
if (!configManager.isToggled()) {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', "&cKnockbacksync is currently disabled on this server. Contact your server administrator for more information."));
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', serverDisabledMessage));
} else {
togglePlayerKnockback(target, configManager, sender);
togglePlayerKnockback(target, sender);
}
}
})
@@ -72,12 +93,18 @@ public class ToggleCommand implements BuilderCommand {
}
private static void toggleGlobalKnockback(ConfigManager configManager, Sender 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);
KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().set("enabled", toggledState);
KnockbackSyncBase.INSTANCE.getConfigManager().saveConfig();
configManager.getConfigWrapper().set("enabled", toggledState);
configManager.saveConfig();
String message = ChatUtil.translateAlternateColorCodes('&',
toggledState ? configManager.getEnableMessage() : configManager.getDisableMessage()
@@ -85,10 +112,16 @@ public class ToggleCommand implements BuilderCommand {
sender.sendMessage(ChatUtil.translateAlternateColorCodes('&', message));
}
private static void togglePlayerKnockback(PlatformPlayer target, ConfigManager configManager, Sender sender) {
UUID uuid = target.getUUID();
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(uuid)) {
if (PlayerDataManager.shouldExempt(target.getUUID())) {
String message = ChatUtil.translateAlternateColorCodes('&',
configManager.getPlayerIneligibleMessage()
).replace("%player%", target.getName());
@@ -97,11 +130,14 @@ public class ToggleCommand implements BuilderCommand {
return;
}
boolean hasPlayerData = PlayerDataManager.containsPlayerData(uuid);
if (hasPlayerData)
PlayerDataManager.removePlayerData(uuid);
else {
PlayerDataManager.addPlayerData(uuid, new PlayerData(KnockbackSyncBase.INSTANCE.platformServer.getPlayer(uuid)));
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('&',
@@ -1,8 +1,11 @@
package me.caseload.knockbacksync.command.subcommand;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.ConfigWrapper;
import me.caseload.knockbacksync.command.generic.BuilderCommand;
import me.caseload.knockbacksync.permission.PermissionChecker;
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;
@@ -12,7 +15,14 @@ import java.util.function.Predicate;
public class ToggleOffGroundSubcommand implements BuilderCommand {
@Override
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")
@@ -26,14 +36,26 @@ public class ToggleOffGroundSubcommand implements BuilderCommand {
return PredicatePermission.of(senderPredicate).testPermission(sender);
}))
.handler(commandContext -> {
boolean toggledState = !KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().getBoolean("enable_experimental_offground", false);
KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().set("enable_experimental_offground", toggledState);
KnockbackSyncBase.INSTANCE.getConfigManager().saveConfig();
offGroundSyncEnabled = !offGroundSyncEnabled;
Base.INSTANCE.getConfigManager().getConfigWrapper().set("enable_offground_synchronization", offGroundSyncEnabled);
Base.INSTANCE.getConfigManager().saveConfig();
String message = ChatUtil.translateAlternateColorCodes('&',
toggledState ?
KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().getString("enable_experimental_offground_message", "&aSuccessfully enabled offground experiment.") :
KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().getString("disable_experimental_offground_message", "&cSuccessfully disabled offground experiment."));
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);
}
}
@@ -1,16 +1,20 @@
package me.caseload.knockbacksync.listener;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
public abstract class PlayerDamageListener {
public void onPlayerDamage(PlatformPlayer victim, PlatformPlayer attacker) {
if (!KnockbackSyncBase.INSTANCE.getConfigManager().isToggled())
if (!Base.INSTANCE.getConfigManager().isToggled())
return;
PlayerData playerData = PlayerDataManager.getPlayerData(victim.getUUID());
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;
@@ -18,7 +22,7 @@ public abstract class PlayerDamageListener {
playerData.setLastDamageTicks(victim.getNoDamageTicks());
playerData.updateCombat();
if (!KnockbackSyncBase.INSTANCE.getConfigManager().isRunnableEnabled())
playerData.sendPing(false);
if (!Base.INSTANCE.getConfigManager().isRunnableEnabled())
playerData.sendPing(true);
}
}
@@ -1,33 +0,0 @@
package me.caseload.knockbacksync.listener;
import me.caseload.knockbacksync.KnockbackSyncBase;
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 java.util.UUID;
public abstract class PlayerJoinQuitListener {
public void onPlayerJoin(PlayerData player) {
PlayerDataManager.addPlayerData(player.getUuid(), player);
PlatformPlayer platformPlayer = player.getPlatformPlayer();
if (KnockbackSyncBase.INSTANCE.getConfigManager().isUpdateAvailable() && KnockbackSyncBase.INSTANCE.getConfigManager().isNotifyUpdate() && KnockbackSyncBase.INSTANCE.getPermissionChecker().hasPermission(platformPlayer, "knockbacksync.update"))
platformPlayer.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(UUID uuid) {
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
if (playerData == null)
return;
if (playerData.isInCombat())
playerData.quitCombat(true);
PlayerDataManager.removePlayerData(uuid);
}
}
@@ -1,7 +1,10 @@
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.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import me.caseload.knockbacksync.player.PlatformPlayer;
import me.caseload.knockbacksync.player.PlayerData;
@@ -9,24 +12,44 @@ import me.caseload.knockbacksync.player.PlayerData;
public abstract class PlayerKnockbackListener {
public void onPlayerVelocity(PlatformPlayer victim, Vector3d velocity) {
if (!KnockbackSyncBase.INSTANCE.getConfigManager().isToggled())
if (!Base.INSTANCE.getConfigManager().isToggled())
return;
PlayerData playerData = PlayerDataManager.getPlayerData(victim.getUUID());
if (playerData == null)
User user = victim.getUser();
if (user == null) return; // Prevent errors with players disconnecting while this is running (or with fake player?)
PlayerData victimPlayerData = PlayerDataManager.getPlayerData(user);
if (victimPlayerData == null)
return;
Integer damageTicks = playerData.getLastDamageTicks();
if (damageTicks != null && damageTicks > 8)
if (victimPlayerData.getNotNullPing() < PlayerData.PING_OFFSET)
return;
Double verticalVelocity = playerData.getVerticalVelocity();
if (verticalVelocity == null || !playerData.isOnGround(velocity.getY()))
double distanceToGround = victimPlayerData.getDistanceToGround();
if (distanceToGround <= 0)
return; // 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;
// Since we're already changing types do we need to use withY to get a new object
// Or can we just go velcoity.y = verticalVelocity ?
Vector3d adjustedVelocity = velocity.withY(verticalVelocity);
victim.setVelocity(adjustedVelocity); // Use PlatformPlayer's setVelocity
Vector3d adjustedVelocity;
if (victimPlayerData.isOnGroundClientSide(velocity.getY(), distanceToGround)) {
Integer damageTicks = victimPlayerData.getLastDamageTicks();
if (damageTicks != null && damageTicks > 8)
return;
adjustedVelocity = velocity.withY(victimPlayerData.getVerticalVelocity()); // Should be impossible to produce a NPE in this context
}
else if (victimPlayerData.isOffGroundSyncEnabled())
adjustedVelocity = velocity.withY(victimPlayerData.getCompensatedOffGroundVelocity());
else
return;
victim.setVelocity(adjustedVelocity);
}
}
@@ -5,7 +5,9 @@ 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.Base;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import me.caseload.knockbacksync.player.PlayerData;
import me.caseload.knockbacksync.util.MathUtil;
@@ -29,21 +31,22 @@ public class AttributeChangeListener extends PacketListenerAbstract {
@Override
public void onPacketSend(PacketSendEvent event) {
if (!Base.INSTANCE.getConfigManager().isToggled()) return;
if (event.getPacketType() == PacketType.Play.Server.UPDATE_ATTRIBUTES) {
WrapperPlayServerUpdateAttributes packet = new WrapperPlayServerUpdateAttributes(event);
UUID uuid = event.getUser().getUUID();
if (!PlayerDataManager.containsPlayerData(uuid))
return;
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(uuid, calculateValueWithModifiers(property));
onPlayerGravityChange(user, calculateValueWithModifiers(property));
} else if (property.getAttribute().equals(Attributes.GENERIC_KNOCKBACK_RESISTANCE)) {
onPlayerKnockBackChange(uuid, calculateValueWithModifiers(property));
onPlayerKnockBackChange(user, calculateValueWithModifiers(property));
}
}
}
@@ -84,8 +87,8 @@ 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(UUID uuid, double newGravity) {
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
public void onPlayerGravityChange(User user, double newGravity) {
PlayerData playerData = PlayerDataManager.getPlayerData(user);
if (playerData.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_5)) {
playerData.setGravityAttribute(newGravity);
} else {
@@ -93,8 +96,8 @@ public class AttributeChangeListener extends PacketListenerAbstract {
}
}
private void onPlayerKnockBackChange(UUID uuid, double newKnockbackResistance) {
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
private void onPlayerKnockBackChange(User user, double newKnockbackResistance) {
PlayerData playerData = PlayerDataManager.getPlayerData(user);
playerData.setKnockbackResistanceAttribute(newKnockbackResistance);
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -1,107 +1,92 @@
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.event.PacketSendEvent;
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 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.KnockbackSyncBase;
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.List;
import java.util.Queue;
import java.util.UUID;
public class PingReceiveListener extends PacketListenerAbstract {
// TODO supplment our own ping calculations with over keepalive sent by the server with
@Override
public void onPacketSend(PacketSendEvent event) {
PacketTypeCommon packetType = event.getPacketType();
if (packetType.equals(PacketType.Play.Server.KEEP_ALIVE)) {
WrapperPlayServerKeepAlive keepAlive = new WrapperPlayServerKeepAlive(event);
long id = keepAlive.getId();
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
playerData.keepaliveMap.add(new Pair<>(id, System.nanoTime()));
} else if (packetType.equals(PacketType.Play.Server.PING)) {
WrapperPlayServerPing ping = new WrapperPlayServerPing(event);
int id = ping.getId();
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
playerData.transactionsSent.add(new Pair<>(id, System.nanoTime()));
} else if (packetType.equals(PacketType.Play.Server.WINDOW_CONFIRMATION)) {
WrapperPlayServerWindowConfirmation confirmation = new WrapperPlayServerWindowConfirmation(event);
int id = confirmation.getActionId();
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
playerData.transactionsSent.add(new Pair<>(id, System.nanoTime()));
}
public PingReceiveListener() {
super(PacketListenerPriority.LOWEST);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (!KnockbackSyncBase.INSTANCE.getConfigManager().isToggled()) return;
// 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();
if (packetType == PacketType.Play.Client.KEEP_ALIVE) {
User user = event.getUser();
if (user == null) return;
PlayerData playerData = PlayerDataManager.getPlayerData(user);
if (playerData == null) return;
if (playerData.pingStrategy == PingStrategy.KEEPALIVE && packetType == PacketType.Play.Client.KEEP_ALIVE) {
WrapperPlayClientKeepAlive keepAlive = new WrapperPlayClientKeepAlive(event);
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
long receivedId = keepAlive.getId();
handlePingCalculationPackets(event, playerData, receivedId, playerData.keepaliveMap);
} else if (packetType == PacketType.Play.Client.PONG) {
} else if (playerData.pingStrategy == PingStrategy.TRANSACTION && packetType == PacketType.Play.Client.PONG) {
WrapperPlayClientPong pong = new WrapperPlayClientPong(event);
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
int id = pong.getId();
handlePingCalculationPackets(event, playerData, id, playerData.transactionsSent);
} else if (packetType == PacketType.Play.Client.WINDOW_CONFIRMATION) {
} else if (playerData.pingStrategy == PingStrategy.TRANSACTION && packetType == PacketType.Play.Client.WINDOW_CONFIRMATION) {
WrapperPlayClientWindowConfirmation windowConfirmation = new WrapperPlayClientWindowConfirmation(event);
PlayerData playerData = PlayerDataManager.getPlayerData(event.getUser().getUUID());
if (playerData == null) return;
int id = windowConfirmation.getActionId();
handlePingCalculationPackets(event, playerData, id, playerData.transactionsSent);
}
}
private <T extends Number> void handlePingCalculationPackets(PacketReceiveEvent event, PlayerData playerData, long id, List<Pair<T, Long>> packetSentList) {
long pingNanos = (System.nanoTime() - packetSentList.remove(0).getSecond());
double diffMillisDouble = pingNanos / 1_000_000.0;
private <T extends Number> void handlePingCalculationPackets(PacketReceiveEvent event, 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());
playerData.setPreviousPing(playerData.getPing());
playerData.setPing(diffMillisDouble);
playerData.getJitterCalculator().addPing(pingNanos);
double jitter = playerData.getJitterCalculator().calculateJitter();
playerData.setJitter(jitter);
// We can cancel for all 3 cases
if (playerData.didWeSendThatPacket(id)) {
// Pong not needed as vanilla ignores the packet, its needed for packet limiters
// We want to cancel Window Confirmations that we send, and ...
// Minecraft kicks players that send invalid keepAliveID packets
// Since Minecraft doesn't know we just sent a keepalive, we gotta cancel it
// To stop MC from processing it and kicking the player
event.setCancelled(true);
}
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());
}
}
@@ -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()));
}
}
}
@@ -1,5 +1,6 @@
package me.caseload.knockbacksync.manager;
import com.github.retrooper.packetevents.protocol.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
@@ -8,17 +9,17 @@ import java.util.concurrent.ConcurrentHashMap;
public class CombatManager {
private static final Set<UUID> combatPlayers = ConcurrentHashMap.newKeySet();
private static final Set<@NotNull User> combatPlayers = ConcurrentHashMap.newKeySet();
public static @NotNull Set<UUID> getPlayers() {
public static @NotNull Set<@NotNull User> getPlayers() {
return combatPlayers;
}
public static void addPlayer(UUID uuid) {
combatPlayers.add(uuid);
public static void addPlayer(@NotNull User user) {
combatPlayers.add(user);
}
public static void removePlayer(UUID uuid) {
combatPlayers.remove(uuid);
public static void removePlayer(@NotNull User user) {
combatPlayers.remove(user);
}
}
@@ -1,17 +1,17 @@
package me.caseload.knockbacksync.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import lombok.Getter;
import lombok.Setter;
import me.caseload.knockbacksync.ConfigWrapper;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -19,12 +19,13 @@ import java.util.Map;
@Setter
public class ConfigManager {
public static final long CONFIG_VERSION = 2;
public static final long CONFIG_VERSION = 7;
private boolean toggled;
private boolean runnableEnabled;
private boolean updateAvailable;
private boolean notifyUpdate;
private boolean autoUpdate;
private long runnableInterval;
private long combatTimer;
@@ -35,19 +36,19 @@ public class ConfigManager {
private String playerEnableMessage;
private String playerDisableMessage;
private String playerIneligibleMessage;
private String reloadMessage;
private String playerDisconnectedWhileExecutingCommand;
private AbstractTaskHandle pingTask;
private Map<String, Object> config;
private File configFile;
private ObjectMapper mapper;
private ConfigWrapper configWrapper; // Cache the ConfigWrapper instance
private YamlConfiguration yamlConfig;
public ConfigManager() {
mapper = new ObjectMapper(new YAMLFactory());
KnockbackSyncBase instance = KnockbackSyncBase.INSTANCE;
Base instance = Base.INSTANCE;
configFile = new File(instance.getDataFolder(), "config.yml");
yamlConfig = new YamlConfiguration(configFile);
}
public ConfigWrapper getConfigWrapper() {
@@ -60,9 +61,10 @@ public class ConfigManager {
public void reloadConfig() {
try {
if (!configFile.exists()) {
KnockbackSyncBase.INSTANCE.saveDefaultConfig();
Base.INSTANCE.saveDefaultConfig();
}
config = mapper.readValue(configFile, Map.class);
yamlConfig.load();
config = yamlConfig.getData();
configWrapper = new ConfigWrapper(config);
} catch (IOException e) {
e.printStackTrace();
@@ -73,7 +75,8 @@ public class ConfigManager {
public void saveConfig() {
try {
mapper.writeValue(configFile, config);
yamlConfig.setData(config);
yamlConfig.save();
} catch (IOException e) {
e.printStackTrace();
}
@@ -84,40 +87,43 @@ public class ConfigManager {
reloadConfig();
}
ConfigWrapper config = getConfigWrapper(); // Use cached ConfigWrapper
ConfigWrapper configWrapper = getConfigWrapper(); // Use cached ConfigWrapper
toggled = config.getBoolean("enabled", true);
updateConfig();
toggled = configWrapper.getBoolean("enabled", true);
// Checks to see if the runnable was enabled...
// and if we now want to disable it
boolean newRunnableEnabled = config.getBoolean("runnable.enabled", true);
boolean newRunnableEnabled = configWrapper.getBoolean("runnable.enabled", true);
if (runnableEnabled && newRunnableEnabled && pingTask != null) { // null check for first startup
pingTask.cancel();
}
runnableEnabled = newRunnableEnabled;
runnableInterval = config.getLong("runnable.interval", 5L);
runnableInterval = configWrapper.getLong("runnable.interval", 5L);
if (runnableEnabled) {
long initialDelay = 0L;
long pingTaskRunnableInterval = runnableInterval;
// Folia does not allow 0 ticks of wait time
if (KnockbackSyncBase.INSTANCE.platform == Platform.FOLIA) {
if (Base.INSTANCE.getPlatform() == Platform.FOLIA) {
initialDelay = 1L;
pingTaskRunnableInterval = Math.max(pingTaskRunnableInterval, 1L);
}
pingTask = KnockbackSyncBase.INSTANCE.getScheduler().runTaskTimerAsynchronously(new PingRunnable(), initialDelay, pingTaskRunnableInterval);
pingTask = Base.INSTANCE.getScheduler().runTaskTimerAsynchronously(new PingRunnable(), initialDelay, pingTaskRunnableInterval);
}
notifyUpdate = config.getBoolean("notify_updates", true);
combatTimer = config.getLong("runnable.timer", 30L);
spikeThreshold = config.getLong("spike_threshold", 20L);
enableMessage = config.getString("enable_message", "&aSuccessfully enabled KnockbackSync.");
disableMessage = config.getString("disable_message", "&cSuccessfully disabled KnockbackSync.");
playerEnableMessage = config.getString("player_enable_message", "&aSuccessfully enabled KnockbackSync for %player%.");
playerDisableMessage = config.getString("player_disable_message", "&cSuccessfully disabled KnockbackSync for %player%.");
playerIneligibleMessage = config.getString("player_ineligible_message", "&c%player% is ineligible for KnockbackSync. If you believe this is an error, please open an issue on the github page.");
reloadMessage = config.getString("reload_message", "&aSuccessfully reloaded KnockbackSync.");
notifyUpdate = configWrapper.getBoolean("notify_updates", true);
autoUpdate = configWrapper.getBoolean("auto_update", true);
combatTimer = configWrapper.getLong("runnable.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() {
@@ -125,16 +131,20 @@ public class ConfigManager {
long oldConfigVersion = oldConfig.getLong("config_version", 0);
if (oldConfigVersion < CONFIG_VERSION) {
// Backup old config
// Backup old config with comments
File backupFile = new File(configFile.getParentFile(), "config-version-" + oldConfigVersion + ".yml");
if (configFile.renameTo(backupFile)) {
KnockbackSyncBase.INSTANCE.getLogger().info("Backed up old config to " + backupFile.getName());
} else {
KnockbackSyncBase.INSTANCE.getLogger().warning("Failed to backup old config");
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
KnockbackSyncBase.INSTANCE.saveDefaultConfig();
Base.INSTANCE.saveDefaultConfig();
reloadConfig();
// Transfer existing settings
@@ -147,17 +157,17 @@ public class ConfigManager {
// Save updated config
saveConfig();
KnockbackSyncBase.INSTANCE.getLogger().info("Config updated to version " + CONFIG_VERSION);
Base.INSTANCE.getLogger().info("Config updated to version " + CONFIG_VERSION);
}
}
private void transferAllSettings(ConfigWrapper oldConfig, ConfigWrapper newConfig) {
transferSettingsRecursive(oldConfig, newConfig, "");
transferSettingsRecursive(oldConfig, newConfig, ".");
}
private void transferSettingsRecursive(ConfigWrapper oldConfig, ConfigWrapper newConfig, String currentPath) {
for (String key : oldConfig.getKeys(currentPath)) {
String fullPath = currentPath.isEmpty() ? key : currentPath + "." + key;
String fullPath = currentPath.isEmpty() || currentPath.equals(".") ? key : currentPath + "." + key;
if (newConfig.contains(fullPath)) {
Object value = oldConfig.get(fullPath);
if (value instanceof Map) {
@@ -1,9 +1,13 @@
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;
@@ -11,23 +15,28 @@ import java.util.concurrent.ConcurrentHashMap;
public class PlayerDataManager {
private static final Map<UUID, PlayerData> playerDataMap = new ConcurrentHashMap<>();
private static final Map<User, PlayerData> playerDataMap = new ConcurrentHashMap<>();
public static PlayerData getPlayerData(@NotNull UUID uuid) {
return playerDataMap.get(uuid);
public static @Nullable PlayerData getPlayerData(@NotNull User user) {
return playerDataMap.get(user);
}
public static void addPlayerData(@NotNull UUID uuid, @NotNull PlayerData playerData) {
if (!shouldExempt(uuid))
playerDataMap.put(uuid, playerData);
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 UUID uuid) {
playerDataMap.remove(uuid);
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 UUID uuid) {
return playerDataMap.containsKey(uuid);
public static boolean containsPlayerData(@NotNull User user) {
return playerDataMap.containsKey(user);
}
public static boolean shouldExempt(@NotNull UUID uuid) {
@@ -0,0 +1,6 @@
package me.caseload.knockbacksync.player;
public enum PingStrategy {
KEEPALIVE,
TRANSACTION
}
@@ -1,8 +0,0 @@
package me.caseload.knockbacksync.player;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
public interface PlatformBlock {
WrappedBlockState getMaterial();
// Add more methods as needed
}
@@ -1,5 +1,7 @@
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;
@@ -43,5 +45,22 @@ public interface PlatformPlayer {
@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
}
@@ -7,8 +7,7 @@ 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.states.WrappedBlockState;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
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;
@@ -16,39 +15,45 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPi
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowConfirmation;
import lombok.Getter;
import lombok.Setter;
import me.caseload.knockbacksync.KnockbackSyncBase;
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.incendo.cloud.CloudCapability;
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.ConcurrentHashMap;
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;
public static float TICK_RATE = 20.0F;
private static Field playerField;
public final List<Pair<Integer, Long>> transactionsSent = new LinkedList<>();
public final List<Pair<Long, Long>> keepaliveMap = new LinkedList<>();
private static final short MAIN_THREAD_TRANSACTION_ID = -1;
private static final short NETTY_THREAD_TRANSACTION_ID = -2;
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 (KnockbackSyncBase.INSTANCE.platform) {
switch (Base.INSTANCE.getPlatform()) {
case BUKKIT:
case FOLIA:
Class<?> bukkitPlayerClass = Class.forName("me.caseload.knockbacksync.player.BukkitPlayer");
@@ -59,7 +64,7 @@ public class PlayerData {
playerField = fabricPlayerClass.getDeclaredField("fabricPlayer");
break;
default:
throw new IllegalStateException("Unexpected platform: " + KnockbackSyncBase.INSTANCE.platform);
throw new IllegalStateException("Unexpected platform: " + Base.INSTANCE.getPlatform());
}
} catch (Exception e) {
e.printStackTrace();
@@ -75,31 +80,27 @@ public class PlayerData {
@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;
@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(PlatformPlayer platformPlayer) {
public PlayerData(User user, PlatformPlayer platformPlayer) {
this.uuid = platformPlayer.getUUID();
this.user = user;
this.platformPlayer = platformPlayer;
this.pingStrategy = loadPingStrategy(Base.INSTANCE.getConfigManager());
}
User tempUser = null;
public double getNotNullPing() {
return ping != null ? ping : platformPlayer.getPing();
}
PlayerManager playerManager = PacketEvents.getAPI().getPlayerManager();
Object player = null;
try {
player = playerField.get(platformPlayer);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (player != null) {
tempUser = playerManager.getUser(player);
}
this.user = tempUser;
public double getNotNullPreviousPing() {
return previousPing != null ? previousPing : platformPlayer.getPing();
}
/**
@@ -109,24 +110,31 @@ public class PlayerData {
*
* @return The compensated ping, with a minimum of 1.
*/
public double getEstimatedPing() {
double currentPing = (ping != null) ? ping : platformPlayer.getPing();
double lastPing = (previousPing != null) ? previousPing : platformPlayer.getPing();
double ping = (currentPing - lastPing > KnockbackSyncBase.INSTANCE.getConfigManager().getSpikeThreshold()) ? lastPing : currentPing;
public double getCompensatedPing() {
double ping = getNotNullPing();
double previousPing = getNotNullPreviousPing();
double spikeCompensatedPing = (ping - previousPing > Base.INSTANCE.getConfigManager().getSpikeThreshold()) ? previousPing : ping;
return Math.max(1, ping - PING_OFFSET);
return Math.max(1, spikeCompensatedPing - PING_OFFSET);
}
// public boolean isKeepAliveIDOurs(long id) {
// return id == lastKeepAliveID;
// }
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;
String pingStrategy = KnockbackSyncBase.INSTANCE.getConfigManager().getConfigWrapper().getString("ping_strategy", "KEEPALIVE");
switch (pingStrategy) {
case "KEEPALIVE":
case KEEPALIVE:
long keepAliveID = async ? NETTY_THREAD_TRANSACTION_ID : MAIN_THREAD_TRANSACTION_ID;
if (async) {
ChannelHelper.runInEventLoop(user.getChannel(), () -> {
@@ -139,8 +147,7 @@ public class PlayerData {
user.sendPacket(new WrapperPlayServerKeepAlive(keepAliveID));
}
break;
case "PING":
case "TRANSACTION":
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)) {
@@ -176,32 +183,37 @@ public class PlayerData {
* @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 isOnGround(double verticalVelocity) {
WrappedBlockState blockState = platformPlayer.getWorld().getBlockStateAt(platformPlayer.getLocation());
if (platformPlayer.isGliding() ||
blockState.getType() == StateTypes.WATER ||
blockState.getType() == StateTypes.LAVA ||
blockState.getType() == StateTypes.COBWEB ||
blockState.getType() == StateTypes.SCAFFOLDING) {
return false;
}
if (ping == null || ping < PING_OFFSET)
return false;
double gDist = getDistanceToGround();
if (gDist <= 0)
return false; // prevent player from taking adjusted knockback when on ground serverside
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 + gDist, gravityAttribute);
int tFall = MathUtil.calculateFallTime(verticalVelocity, mH + distanceToGround, gravityAttribute);
if (tFall == -1)
return false; // reached the max tick limit, not safe to predict
return getEstimatedPing() >= tMax + tFall / TICK_RATE * 1000 && gDist <= 1.3;
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());
}
/**
@@ -218,10 +230,10 @@ public class PlayerData {
for (Vector3d corner : getBBCorners()) {
RayTraceResult result = world.rayTraceBlocks(corner, new Vector3d(0, -1, 0), 5, FluidHandling.NONE, true);
if (result == null || result.getHitPosition() == null)
if (result == null || result.getHitBlock() == null)
continue;
collisionDist = Math.min(collisionDist, corner.getY() - result.getHitPosition().getY());
collisionDist = Math.min(collisionDist, corner.getY() - result.getHitBlockPosition().getY());
}
return collisionDist - 1;
@@ -233,16 +245,16 @@ public class PlayerData {
* @return An array of locations representing the corners of the bounding box.
*/
private Vector3d[] getBBCorners() {
Vector3d playerPos = platformPlayer.getLocation();
double width = 0.6; // typical player width
// double height = 1.8; // typical player height
double adjustment = 0.01;
BoundingBox boundingBox = platformPlayer.getBoundingBox();
Vector3d location = platformPlayer.getLocation();
return new Vector3d[]{
new Vector3d(playerPos.getX() - width / 2 + adjustment, playerPos.getY(), playerPos.getZ() - width / 2 + adjustment),
new Vector3d(playerPos.getX() - width / 2 + adjustment, playerPos.getY(), playerPos.getZ() + width / 2 - adjustment),
new Vector3d(playerPos.getX() + width / 2 - adjustment, playerPos.getY(), playerPos.getZ() - width / 2 + adjustment),
new Vector3d(playerPos.getX() + width / 2 - adjustment, playerPos.getY(), playerPos.getZ() + width / 2 - adjustment)
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)
};
}
@@ -258,7 +270,6 @@ public class PlayerData {
if (!attacker.isSprinting()) {
yAxis = 0.36080000519752503;
// double knockbackResistance = knockbackResistanceAttribute;
double resistanceFactor = 0.04000000119 * knockbackResistanceAttribute * 10;
yAxis -= resistanceFactor;
}
@@ -270,43 +281,41 @@ public class PlayerData {
return yAxis;
}
// might need soon
// public double calculateJumpVelocity() {
// double jumpVelocity = 0.42;
//
// PotionEffect jumpEffect = player.getPotionEffect(PotionEffectType.JUMP);
// if (jumpEffect != null) {
// int amplifier = jumpEffect.getAmplifier();
// jumpVelocity += (amplifier + 1) * 0.1F;
// }
//
// return jumpVelocity;
// }
public boolean isInCombat() {
return combatTask != null;
}
public void updateCombat() {
if (isInCombat())
combatTask.cancel();
combatTask = newCombatTask();
CombatManager.addPlayer(uuid);
Channel channel = (Channel) user.getChannel();
channel.eventLoop().execute(() -> {
if (combatTask != null) {
combatTask.cancel();
}
combatTask = newCombatTask(channel);
CombatManager.addPlayer(user);
});
}
public void quitCombat(boolean cancelTask) {
if (cancelTask)
combatTask.cancel();
combatTask = null;
CombatManager.removePlayer(uuid);
/**
* 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() {
return KnockbackSyncBase.INSTANCE.getScheduler().runTaskLaterAsynchronously(
() -> quitCombat(false), KnockbackSyncBase.INSTANCE.getConfigManager().getCombatTimer());
private AbstractTaskHandle newCombatTask(Channel channel) {
return new NettyTaskHandle(channel.eventLoop().schedule(() -> quitCombat(false), convertTicksToMilliseconds(Base.INSTANCE.getConfigManager().getCombatTimer()), TimeUnit.MILLISECONDS));
}
public ClientVersion getClientVersion() {
@@ -327,4 +336,34 @@ public class PlayerData {
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) {
String pingStrategy = configManager.getConfigWrapper().getString("ping_strategy", "KEEPALIVE");
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;
}
}
@@ -1,6 +1,7 @@
package me.caseload.knockbacksync.runnable;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -11,12 +12,13 @@ public class PingRunnable implements Runnable {
@Override
public void run() {
if (!KnockbackSyncBase.INSTANCE.getConfigManager().isToggled())
if (!Base.INSTANCE.getConfigManager().isToggled())
return;
for (UUID uuid : CombatManager.getPlayers()) {
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
playerData.sendPing(true);
for (User user : CombatManager.getPlayers()) {
PlayerData playerData = PlayerDataManager.getPlayerData(user);
if (playerData != null)
playerData.sendPing(true);
}
}
}
@@ -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);
}
}
@@ -1,6 +1,6 @@
package me.caseload.knockbacksync.sender;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import java.util.UUID;
@@ -10,7 +10,7 @@ import java.util.UUID;
* @param <T> the command sender type
*/
public final class AbstractSender<T> implements Sender {
private final KnockbackSyncBase plugin;
private final Base plugin;
private final SenderFactory<?, T> factory;
private final T sender;
@@ -18,7 +18,7 @@ public final class AbstractSender<T> implements Sender {
private final String name;
private final boolean isConsole;
AbstractSender(KnockbackSyncBase plugin, SenderFactory<?, T> factory, T sender) {
AbstractSender(Base plugin, SenderFactory<?, T> factory, T sender) {
this.plugin = plugin;
this.factory = factory;
this.sender = sender;
@@ -28,7 +28,7 @@ public final class AbstractSender<T> implements Sender {
}
@Override
public KnockbackSyncBase getPlugin() {
public Base getPlugin() {
return this.plugin;
}
@@ -1,6 +1,6 @@
package me.caseload.knockbacksync.sender;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import java.util.UUID;
@@ -23,7 +23,7 @@ public interface Sender {
*
* @return the plugin
*/
KnockbackSyncBase getPlugin();
Base getPlugin();
/**
* Gets the sender's username
@@ -1,6 +1,6 @@
package me.caseload.knockbacksync.sender;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import java.util.Objects;
import java.util.UUID;
@@ -11,7 +11,7 @@ import java.util.UUID;
* @param <P> the plugin type
* @param <T> the command sender type
*/
public abstract class SenderFactory<P extends KnockbackSyncBase, T> implements AutoCloseable {
public abstract class SenderFactory<P extends Base, T> implements AutoCloseable {
private final P plugin;
public SenderFactory(P plugin) {
@@ -2,7 +2,8 @@ package me.caseload.knockbacksync.stats.custom;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -18,8 +19,7 @@ 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 = KnockbackSyncBase.INSTANCE.getDataFolder();
public static URL jarUrl;
private static final File dataFolder = Base.INSTANCE.getDataFolder();
private static String cachedBuildType = null;
public BuildTypePie() {
@@ -35,7 +35,7 @@ public class BuildTypePie extends SimplePie {
private static String calculateBuildType() {
try {
String currentHash = KnockbackSyncBase.INSTANCE.getPluginJarHashProvider().getPluginJarHash();
String currentHash = Base.INSTANCE.getPluginJarHashProvider().getPluginJarHash();
downloadBuildFiles();
if (isHashInFile(currentHash, new File(dataFolder, RELEASES_FILE))) {
@@ -53,12 +53,12 @@ public class BuildTypePie extends SimplePie {
private static void downloadBuildFiles() throws IOException {
GitHub gitHub = GitHub.connectAnonymously();
GHRelease latestRelease = gitHub.getRepository("CASELOAD7000/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)) {
KnockbackSyncBase.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();
@@ -72,7 +72,7 @@ public class BuildTypePie extends SimplePie {
}
}
KnockbackSyncBase.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
Base.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
}
}
}
@@ -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;
});
}
}
@@ -1,5 +1,9 @@
package me.caseload.knockbacksync.stats.custom;
import me.caseload.knockbacksync.stats.CustomChart;
public interface Metrics {
void addCustomChart(CustomChart chart);
void shutdown();
}
@@ -1,6 +1,8 @@
package me.caseload.knockbacksync.stats.custom;
import me.caseload.knockbacksync.KnockbackSyncBase;
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;
@@ -15,9 +17,13 @@ public class PlayerVersionsPie extends AdvancedPie {
public PlayerVersionsPie() {
super("player_version", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (PlatformPlayer player : KnockbackSyncBase.INSTANCE.platformServer.getOnlinePlayers()) {
PlayerData playerData = PlayerDataManager.getPlayerData(player.getUUID());
valueMap.put(playerData.getClientVersion().toString(), valueMap.getOrDefault(playerData.getClientVersion().toString(), 0) + 1);
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;
});
@@ -1,13 +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() {
throw new IllegalStateException("Empty stats class!");
Base.INSTANCE.getScheduler().runTaskAsynchronously(() -> {
BuildTypePie.determineBuildType(); // Function to calculate hash
metrics.addCustomChart(new PlayerVersionsPie());
metrics.addCustomChart(new BuildTypePie());
metrics.addCustomChart(new ClientBrandsPie());
});
}
}
@@ -1,13 +1,10 @@
package me.caseload.knockbacksync.util;
import com.google.common.base.Preconditions;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import me.caseload.knockbacksync.player.PlayerData;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
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();
@@ -21,36 +18,4 @@ public class ChatUtil {
return new String(b);
}
public static String getPingMessage(UUID senderUUID, UUID targetUUID) {
String noun = "";
PlayerData playerData = null;
if (senderUUID == null) {
senderUUID = targetUUID;
}
if (senderUUID.equals(targetUUID) || targetUUID == null) {
playerData = PlayerDataManager.getPlayerData(senderUUID);
if (playerData == null) {
return ChatUtil.translateAlternateColorCodes('&', "&cKnockback synchronization is currently disabled for you!");
}
noun = "Your";
} else {
playerData = PlayerDataManager.getPlayerData(targetUUID);
if (playerData == null) {
return ChatUtil.translateAlternateColorCodes('&', "&cKnockback synchronization is currently disabled for the target player.");
}
noun = playerData.getPlatformPlayer().getName() + "'s";
}
String rawReturnString = null;
if (playerData.getPing() == null) {
rawReturnString = String.format("Pong not received. %s estimated ping is &b" + playerData.getPlatformPlayer().getPing() + "&rms.", noun);
} else {
rawReturnString = String.format("%s last ping packet took &b %.3f&rms. Jitter: &b%.3f&rms.", noun, playerData.getPing(), playerData.getJitter());
}
return ChatUtil.translateAlternateColorCodes('&', rawReturnString);
}
}
@@ -2,7 +2,7 @@ package me.caseload.knockbacksync.util;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.util.reflection.Reflection;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -19,18 +19,9 @@ public class GeyserUtil {
public static boolean isGeyserPlayer(UUID uuid) {
if (!CHECKED_FOR_GEYSER) {
try {
switch (KnockbackSyncBase.INSTANCE.platform) {
case BUKKIT:
case FOLIA:
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
GEYSER_CLASS = classLoader.loadClass("org.geysermc.api.Geyser");
break;
case FABRIC:
GEYSER_CLASS = Class.forName("org.geysermc.api.Geyser");
GEYSER_PRESENT = true;
break;
}
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
GEYSER_CLASS = classLoader.loadClass("org.geysermc.api.Geyser");
GEYSER_PRESENT = true;
} catch (ClassNotFoundException e) {
GEYSER_PRESENT = false;
}
@@ -40,17 +31,8 @@ public class GeyserUtil {
if (GEYSER_PRESENT) {
if (GEYSER_API_CLASS == null) {
try {
switch (KnockbackSyncBase.INSTANCE.platform) {
case BUKKIT:
case FOLIA:
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
GEYSER_API_CLASS = classLoader.loadClass("org.geysermc.api.GeyserApiBase");
break;
case FABRIC:
GEYSER_CLASS = Class.forName("org.geysermc.api.GeyserApiBase");
GEYSER_PRESENT = true;
break;
}
ClassLoader classLoader = PacketEvents.getAPI().getPlugin().getClass().getClassLoader();
GEYSER_API_CLASS = classLoader.loadClass("org.geysermc.api.GeyserApiBase");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
@@ -1,10 +1,24 @@
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 = 20;
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;
@@ -36,17 +50,20 @@ public class MathUtil {
return ticks;
}
public static int calculateTimeToMaxVelocity(double targetVerticalVelocity, double acceleration) {
double a = -acceleration * MULTIPLIER;
double b = acceleration + TERMINAL_VELOCITY * MULTIPLIER;
double c = -2 * targetVerticalVelocity;
public static int calculateTimeToMaxVelocity(double velocity, double acceleration) {
int ticks = 0;
double discriminant = b * b - 4 * a * c;
if (discriminant < 0)
return 0;
while (velocity > 0) {
if (ticks > MAX_TICKS)
return -1;
double positiveRoot = (-b + Math.sqrt(discriminant)) / (2 * a);
return (int) Math.ceil(positiveRoot * 20);
velocity -= acceleration;
velocity = Math.min(velocity, TERMINAL_VELOCITY);
velocity *= MULTIPLIER;
ticks++;
}
return ticks;
}
public static double clamp(double num, double min, double max) {
@@ -10,18 +10,5 @@ public interface PlatformServer {
PlatformPlayer getPlayer(UUID uuid);
// switch (KnockbackSyncBase.INSTANCE.platform) {
// case BUKKIT:
// case FOLIA:
// return Bukkit.getOnlinePlayers().stream()
// .map(BukkitPlayer::new)
// .collect(Collectors.toList());
// case FABRIC:
// MinecraftServer server = (MinecraftServer) FabricLoader.getInstance().getGameInstance();
// return server.getPlayerList().getPlayers().stream()
// .map(FabricPlayer::new)
// .collect(Collectors.toList());
// default:
// throw new IllegalStateException("Unexpected platform: " + KnockbackSyncBase.INSTANCE.platform);
// }
PlatformPlayer getPlayer(Object nativePlatformPlayer);
}
+201 -15
View File
@@ -3,7 +3,7 @@
#########################################
# Do not modify!
config-version: 2
config_version: 7
# Plugin enabled state
# Toggleable using /knockbacksync toggle
@@ -14,11 +14,15 @@ enabled: true
# Required permission: knockbacksync.update
notify_updates: true
# Automatically updates the plugin
# Only servers with restart compatibility can use this feature
auto_update: false
# Grabs the ping of combat-tagged players every x ticks
# Disabling this may lead to inaccuracies in calculations
runnable:
enabled: true # Runnable enabled state
interval: 20 # The interval in ticks between sending out pings to players
interval: 5 # The interval in ticks between sending out pings to players
combat_timer: 30 # The timer in ticks before being considered out of combat
# The minimum change in ping required for it to be considered a lag spike.
@@ -38,17 +42,199 @@ spike_threshold: 20
# ping measured in ms with PING/TRANSACTION is typically within ± 5ms of actual but can be up to 50ms off
ping_strategy: "KEEPALIVE"
# This setting has yet to be implemented. Enable if you want it to be enabled as soon as the next update add support
enable_experimental_offground: false
# Whether to try to detect when player is offground and apply less kb
enable_offground_synchronization: true
enable_message: "&aSuccessfully enabled KnockbackSync."
disable_message: "&cSuccessfully disabled KnockbackSync."
player_enable_message: "&aSuccessfully enabled KnockbackSync for %player%."
player_disable_message: "&aSuccessfully &cdisabled &aKnockbackSync for %player%."
player_ineligible_message: "&c%player% is ineligible for KnockbackSync. If you believe this is an error, please open an issue on the github page."
reload_message: "&aSuccessfully reloaded KnockbackSync config."
global_status_message: "&eGlobal KnockbackSync status: "
player_status_message: "&e%player%'s KnockbackSync status: "
player_disabled_global_message: "&cDisabled (Global toggle is off)"
enable_experimental_offground_message: "&aSuccessfully enabled offground experiment."
disable_experimental_offground_message: "&cSuccessfully disabled offground experiment."
# This is the rate the server will use for (amongst other things) sending packets about entities to players
# In other words it defines how often clients get updated velocity for other entities in the world
# Note that certain events mark velocity as dirty and causes it to be immediately updated for 1 tick regardless of the tick interval
# These include but are not limited to jumping, taking knockback, swimming, and a target entity being affected by a special mob attack
# Such as the Ender Dragon's knockback attacks
# If an entity below doesn't exist in the version of the game your server is running in, changing the config for it will do nothing
# Currently only modifying the player's tick interval is supported on Bukkit/Spigot/Paper + Folia
# Modifying the tick interval of any entity is supported on Fabric.
entity_tick_intervals:
player: 2 # This is 2 by default in vanilla and paper, change to 1 to test out the new feature
allay: 2
area_effect_cloud: Integer.MAX_VALUE
armadillo: 3
armor_stand: 3
arrow: 20
axolotl: 3
bat: 3
bee: 3
blaze: 3
block_display: 1
boat: 3
bogged: 3
breeze: 3
breeze_wind_charge: 10
camel: 3
cat: 3
cave_spider: 3
chest_boat: 3
chest_minecart: 3
chicken: 3
cod: 3
command_block_minecart: 3
cow: 3
creeper: 3
dolphin: 3
donkey: 3
dragon_fireball: 10
drowned: 3
egg: 10
elder_guardian: 3
end_crystal: Integer.MAX_VALUE
ender_dragon: 3
ender_pearl: 10
enderman: 3
endermite: 3
evoker: 3
evoker_fangs: 2
experience_bottle: 10
experience_orb: 20
eye_of_ender: 4
falling_block: 20
firework_rocket: 10
fox: 3
frog: 3
furnace_minecart: 3
ghast: 3
giant: 3
glow_item_frame: Integer.MAX_VALUE
glow_squid: 3
goat: 3
guardian: 3
hoglin: 3
hopper_minecart: 3
horse: 3
husk: 3
illusioner: 3
interaction: 3
iron_golem: 3
item: 20
item_display: 1
item_frame: Integer.MAX_VALUE
ominous_item_spawner: 3
fireball: 10
leash_knot: Integer.MAX_VALUE
lightning_bolt: Integer.MAX_VALUE
llama: 3
llama_spit: 10
magma_cube: 3
marker: 3
minecart: 3
mooshroom: 3
mule: 3
ocelot: 3
painting: Integer.MAX_VALUE
panda: 3
parrot: 3
phantom: 3
pig: 3
piglin: 3
piglin_brute: 3
pillager: 3
polar_bear: 3
potion: 10
pufferfish: 3
rabbit: 3
ravager: 3
salmon: 3
sheep: 3
shulker: 3
shulker_bullet: 3
silverfish: 3
skeleton: 3
skeleton_horse: 3
slime: 3
small_fireball: 10
sniffer: 3
snow_golem: 3
snowball: 10
spawner_minecart: 3
spectral_arrow: 20
spider: 3
squid: 3
stray: 3
strider: 3
tadpole: 3
text_display: 1
tnt: 10
tnt_minecart: 3
trader_llama: 3
trident: 20
tropical_fish: 3
turtle: 3
vex: 3
villager: 3
vindicator: 3
wandering_trader: 3
warden: 3
wind_charge: 10
witch: 3
wither: 3
wither_skeleton: 3
wither_skull: 10
wolf: 3
zoglin: 3
zombie: 3
zombie_horse: 3
zombie_villager: 3
zombified_piglin: 3
fishing_bobber: 5
messages:
toggle:
global:
enable: "&aSuccessfully enabled KnockbackSync."
disable: "&cSuccessfully disabled KnockbackSync."
player:
enable: "&aSuccessfully enabled KnockbackSync for %player%."
disable: "&aSuccessfully &cdisabled &aKnockbackSync for %player%."
ineligible: "&c%player% is ineligible for KnockbackSync. If you believe this is in error, please contact your server administrators."
disconnected-while-executing-command: "&c%player% disconnected while executing command."
permission:
no_global: "&cYou don't have permission to toggle the global setting."
no_self: "&cYou do not have permission to toggle your knockback."
no_other: "&cYou do not have permission to toggle the knockback of other player's."
self:
enable: "&aSuccessfully enabled KnockbackSync."
disable: "&cSuccessfully disabled KnockbackSync."
other:
enable: "&aSuccessfully enabled KnockbackSync for %player%."
disable: "&aSuccessfully &cdisabled &aKnockbackSync for %player%."
ineligible: "&c%player% is ineligible for KnockbackSync. If you believe this is in error, please contact your server administrators."
server_disabled: "&cKnockbacksync is currently disabled on this server. Contact your server administrator for more information."
offground:
enable: "&aSuccessfully enabled offground synchronization."
disable: "&cSuccessfully disabled offground synchronization."
reload:
success: "&aSuccessfully reloaded KnockbackSync."
status:
global:
enabled: "&eKnockbackSync global status: &aEnabled"
disabled: "&eKnockbackSync global status: &cDisabled"
offground:
enabled: "&eKnockbackSync off-ground status: &aEnabled"
disabled: "&eKnockbackSync off-ground status: &cDisabled"
player:
enabled: "&e%player%'s KnockbackSync status: &aEnabled"
disabled: "&e%player%'s KnockbackSync status: &cDisabled"
global_disabled: "&e%player%'s KnockbackSync status: &cDisabled (Global toggle is off)"
permission:
no_self: "&cYou do not have permission to check your knockbacksync status."
no_other: "&cYou do not have permission to check the status of other players!"
console:
must_specify_player: "&cYou must specify a player to use the knockbacksync ping command from the console."
disabled:
self: "&cKnockback synchronization is currently disabled for you!"
target: "&cKnockback synchronization is currently disabled for the target player."
ping:
self:
unavailable: "Accurate ping unavailable. Your estimated ping is &b%ping%&rms."
available: "Your real ping is &b%ping%&rms. Jitter: &b%jitter%&rms. Spike: &b%spike%&r. Compensated ping: &b%compensated%&rms."
other:
unavailable: "Accurate ping unavailable. %player%'s estimated ping is &b%ping%&rms."
available: "%player%'s real ping is &b%ping%&rms. Jitter: &b%jitter%&rms. Spike: &b%spike%&r. Compensated ping: &b%compensated%&rms."
+24 -28
View File
@@ -3,13 +3,12 @@ plugins {
id("com.gradleup.shadow")
}
base {
archivesName.set("${rootProject.property("archives_base_name")}-fabric")
loom {
accessWidenerPath = file("src/main/resources/knockbacksync.accesswidener")
}
val shadeThisThing: Configuration by configurations.creating {
isCanBeConsumed = true
isTransitive = true
base {
archivesName.set("${rootProject.property("archives_base_name")}-fabric")
}
// TODO migrate to only including sourceset for compile, test and javadoc tasks
@@ -26,43 +25,40 @@ tasks.named<JavaCompile>("compileTestJava") {
exclude("**/*")
}
repositories {
maven("https://jitpack.io") // Conditional Mixin
}
dependencies {
implementation(project(":common"))
minecraft("com.mojang:minecraft:${rootProject.property("minecraft_version")}")
mappings(loom.layered {
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-1.21:${rootProject.property("parchment_mappings")}")
})
mappings("net.fabricmc:yarn:${rootProject.property("yarn_mappings")}")
modImplementation("net.fabricmc:fabric-loader:${rootProject.property("loader_version")}")
modImplementation("net.fabricmc.fabric-api:fabric-api:${rootProject.property("fabric_version")}")
modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "${rootProject.property("fabric_version")}"))
modImplementation(fabricApi.module("fabric-events-interaction-v0", "${rootProject.property("fabric_version")}"))
include(modImplementation("me.lucko:fabric-permissions-api:0.3.1")!!)
include(modImplementation("com.github.retrooper:packetevents-fabric:2.5.8-SNAPSHOT")!!)
include(modImplementation("org.incendo:cloud-fabric:2.0.0-beta.9")!!)
include(modImplementation("com.github.retrooper:packetevents-fabric:2.8.0-SNAPSHOT")!!)
include(modImplementation("org.incendo:cloud-fabric:2.0.0-beta.10")!!)
shadeThisThing(implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")!!)
shadeThisThing(implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2")!!)
shadeThisThing(implementation("org.kohsuke:github-api:1.326")!!)
include(implementation("org.incendo:cloud-minecraft-extras:2.0.0-beta.10")!!)
include(implementation("org.yaml:snakeyaml:2.0")!!)
include(implementation("org.kohsuke:github-api:1.326")!!)
// Required for org.kohsuke.github
include(implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")!!)
include(implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.2")!!)
include(implementation("com.fasterxml.jackson.core:jackson-core:2.17.2")!!)
// Not requires in modern Minecraft. May be needed if fabric version is backported to older versions
// include(implementation("org.apache.commons:commons-lang3:3.17.0")!!)
// include(implementation("commons-io:commons-io:2.16.1")!!)
compileOnly("org.geysermc.floodgate:api:2.0-SNAPSHOT")
compileOnly("org.projectlombok:lombok:1.18.34")
compileOnly("io.netty:netty-all:4.1.72.Final")
annotationProcessor("org.projectlombok:lombok:1.18.34")
}
tasks.shadowJar {
archiveClassifier.set("dev")
configurations = listOf(shadeThisThing)
isEnableRelocation = true
relocationPrefix = "${project.property("maven_group")}.${project.property("archives_base_name")}.shaded"
finalizedBy(tasks.remapJar)
}
tasks.remapJar {
archiveClassifier.set(null as String?)
dependsOn(tasks.shadowJar)
inputFile = tasks.shadowJar.get().archiveFile
}
tasks.processResources {
from(project(":common").sourceSets.main.get().resources)
@@ -1,10 +1,10 @@
package me.caseload.knockbacksync;
import com.github.retrooper.packetevents.PacketEvents;
import io.github.retrooper.packetevents.factory.fabric.FabricPacketEventsBuilder;
import io.github.retrooper.packetevents.factory.fabric.FabricPacketEventsAPI;
import lombok.Getter;
import me.caseload.knockbacksync.entity.EntityTickManager;
import me.caseload.knockbacksync.listener.fabric.FabricPlayerDamageListener;
import me.caseload.knockbacksync.listener.fabric.FabricPlayerJoinQuitListener;
import me.caseload.knockbacksync.listener.fabric.FabricPlayerKnockbackListener;
import me.caseload.knockbacksync.listener.fabric.FabricTickRateChangeListener;
import me.caseload.knockbacksync.manager.ConfigManager;
@@ -16,6 +16,7 @@ import me.caseload.knockbacksync.sender.FabricSenderFactory;
import me.caseload.knockbacksync.stats.custom.FabricStatsManager;
import me.caseload.knockbacksync.stats.custom.PluginJarHashProvider;
import me.caseload.knockbacksync.world.FabricServer;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import org.incendo.cloud.execution.ExecutionCoordinator;
@@ -30,14 +31,15 @@ import java.nio.file.Files;
import java.util.Optional;
import java.util.logging.Logger;
public class KBSyncFabricBase extends KnockbackSyncBase {
public class FabricBase extends Base {
private final Logger logger = Logger.getLogger(KBSyncFabricLoaderMod.class.getName());
private final Logger logger = Logger.getLogger(FabricLoaderMod.class.getName());
private final FabricPermissionChecker permissionChecker = new FabricPermissionChecker();
@Getter
private final FabricSenderFactory fabricSenderFactory = new FabricSenderFactory(this);
private float tickRate = 20.0F;
public KBSyncFabricBase() {
public FabricBase() {
super.configManager = new ConfigManager();
super.playerSelectorParser = new FabricPlayerSelectorParser<>();
super.commandManager = new FabricServerCommandManager<>(
@@ -66,8 +68,13 @@ public class KBSyncFabricBase extends KnockbackSyncBase {
@Override
public void load() {
PacketEvents.setAPI(FabricPacketEventsBuilder.build("knockbacksync"));
PacketEvents.getAPI().load();
// Since we JiJ PacketEvents and depend on it on Fabric, we can always just get the API instance since it loads firsts
// No need to make an API instance and load it
}
@Override
public void enable() {
super.enable();
}
@Override
@@ -77,10 +84,10 @@ public class KBSyncFabricBase extends KnockbackSyncBase {
@Override
protected void registerPlatformListeners() {
new FabricPlayerJoinQuitListener().register();
new FabricPlayerDamageListener().register();
new FabricPlayerKnockbackListener().register();
new FabricTickRateChangeListener().register();
super.eventBus.registerStaticListeners(EntityTickManager.class);
}
@Override
@@ -109,7 +116,16 @@ public class KBSyncFabricBase extends KnockbackSyncBase {
return permissionChecker;
}
private URL getJarURL() {
@Override
public float getTickRate() {
return this.tickRate;
}
public void setTickRate(float tickRate) {
this.tickRate = tickRate;
}
public URL getJarURL() {
URL jarUrl = null;
Optional<ModContainer> modContainer = FabricLoader.getInstance().getModContainer("knockbacksync");
if (modContainer.isPresent()) {
@@ -123,4 +139,12 @@ public class KBSyncFabricBase extends KnockbackSyncBase {
}
return jarUrl;
}
public void restartServer() {
}
public boolean hasRestartScript() {
return false;
}
}
@@ -1,14 +1,15 @@
package me.caseload.knockbacksync;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
import net.minecraft.server.MinecraftServer;
public class KBSyncFabricLoaderMod implements PreLaunchEntrypoint, ModInitializer {
public class FabricLoaderMod implements PreLaunchEntrypoint, ModInitializer {
private final KnockbackSyncBase core = new KBSyncFabricBase();
private final Base core = new FabricBase();
public static MinecraftServer getServer() {
return (MinecraftServer) FabricLoader.getInstance().getGameInstance();
@@ -16,19 +17,24 @@ public class KBSyncFabricLoaderMod implements PreLaunchEntrypoint, ModInitialize
@Override
public void onPreLaunch() {
ensureServer();
core.load();
}
@Override
public void onInitialize() {
ensureServer();
core.enable();
core.initializeScheduler();
core.configManager.loadConfig(false);
core.statsManager.init();
core.checkForUpdates();
ServerLifecycleEvents.SERVER_STOPPING.register((server) -> {
core.scheduler.shutdown();
core.statsManager.getMetrics().shutdown();
// bstats removal
// core.statsManager.getMetrics().shutdown();
});
}
private void ensureServer() {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) {
throw new IllegalStateException("This mod can only be run on servers");
}
}
}
@@ -2,23 +2,23 @@ package me.caseload.knockbacksync.callback;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.Vec3d;
public interface PlayerVelocityEvent {
Event<PlayerVelocityEvent> EVENT = EventFactory.createArrayBacked(PlayerVelocityEvent.class,
(listeners) -> (player, velocity) -> {
for (PlayerVelocityEvent listener : listeners) {
InteractionResult result = listener.onVelocityChange(player, velocity);
ActionResult result = listener.onVelocityChange(player, velocity);
if (result != InteractionResult.PASS) {
if (result != ActionResult.PASS) {
return result;
}
}
return InteractionResult.PASS;
return ActionResult.PASS;
});
InteractionResult onVelocityChange(ServerPlayer player, Vec3 velocity);
ActionResult onVelocityChange(ServerPlayerEntity player, Vec3d velocity);
}
@@ -0,0 +1,48 @@
package me.caseload.knockbacksync.entity;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.ConfigWrapper;
import me.caseload.knockbacksync.event.events.ConfigReloadEvent;
import net.minecraft.entity.EntityType;
import me.caseload.knockbacksync.event.KBSyncEventHandler;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class EntityTickManager {
private static final Map<EntityType<?>, Integer> customTickIntervals = new HashMap<>();
static {
updateTickIntervals(Base.INSTANCE.getConfigManager().getConfigWrapper());
}
@KBSyncEventHandler
public static void updateTickIntervals(ConfigReloadEvent event) {
ConfigWrapper configWrapper = event.getConfigManager().getConfigWrapper();
updateTickIntervals(configWrapper);
// Map<String, Integer> entityIntervals = CustomUpdateIntervals.loadEntityIntervalsFromConfig();
// for (ServerLevel level : FabricLoaderMod.getServer().getAllLevels()) {
// CustomUpdateIntervals.updateIntervals(level, entityIntervals);
// }
}
private static void updateTickIntervals(ConfigWrapper configWrapper) {
customTickIntervals.clear();
for (String entityKey : configWrapper.getKeys("entity_tick_intervals")) {
try {
Optional<EntityType<?>> entityType = EntityType.get(entityKey.toLowerCase());
if (entityType.isPresent()) {
int interval = configWrapper.getInt("entity_tick_intervals." + entityKey, entityType.get().getTrackTickInterval());
customTickIntervals.put(entityType.get(), interval);
}
} catch (IllegalArgumentException e) {
System.err.println("Invalid entity type in config: " + entityKey);
}
}
}
public static int getCustomUpdateInterval(EntityType<?> entityType) {
return customTickIntervals.getOrDefault(entityType, entityType.getTrackTickInterval());
}
}
@@ -5,36 +5,24 @@ import me.caseload.knockbacksync.listener.PlayerDamageListener;
import me.caseload.knockbacksync.player.FabricPlayer;
import me.caseload.knockbacksync.player.PlatformPlayer;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
public class FabricPlayerDamageListener extends PlayerDamageListener {
public void register() {
AttackEntityCallback.EVENT.register((player, world, hand, entity, source) -> {
onPlayerDamage((ServerPlayer) player, entity);
return InteractionResult.PASS;
onPlayerDamage((ServerPlayerEntity) player, entity);
return ActionResult.PASS;
});
// Sending ping packets on tick if runnable is disabled
// ServerTickEvents.END_SERVER_TICK.register(server -> {
// if (!KnockbackSyncBase.INSTANCE.getConfigManager().isRunnableEnabled()) {
// for (ServerPlayerEntity player : server.getPlayerList()) {
// PlayerData playerData = PlayerDataManager.getPlayerData(player);
// if (playerData != null) {
// playerData.sendPing();
// }
// }
// }
// });
}
private void onPlayerDamage(ServerPlayer attacker, Entity victimEntity) {
if (victimEntity instanceof Player victim) {
private void onPlayerDamage(ServerPlayerEntity attacker, Entity victimEntity) {
if (victimEntity instanceof PlayerEntity victim) {
PlatformPlayer platformAttacker = new FabricPlayer(attacker);
PlatformPlayer platformVictim = new FabricPlayer((ServerPlayer) victim);
PlatformPlayer platformVictim = new FabricPlayer((ServerPlayerEntity) victim);
super.onPlayerDamage(platformVictim, platformAttacker); // Call the shared method
}
}
@@ -1,17 +0,0 @@
package me.caseload.knockbacksync.listener.fabric;
import me.caseload.knockbacksync.listener.PlayerJoinQuitListener;
import me.caseload.knockbacksync.player.FabricPlayer;
import me.caseload.knockbacksync.player.PlayerData;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
public class FabricPlayerJoinQuitListener extends PlayerJoinQuitListener {
public void register() {
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
onPlayerJoin(new PlayerData(new FabricPlayer(handler.player)));
});
ServerPlayConnectionEvents.DISCONNECT.register(((handler, server) -> {
onPlayerQuit(handler.player.getUUID());
}));
}
}
@@ -4,27 +4,27 @@ import com.github.retrooper.packetevents.util.Vector3d;
import me.caseload.knockbacksync.callback.PlayerVelocityEvent;
import me.caseload.knockbacksync.listener.PlayerKnockbackListener;
import me.caseload.knockbacksync.player.FabricPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.damagesource.DamageTypes;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.damage.DamageTypes;
import net.minecraft.util.ActionResult;
public class FabricPlayerKnockbackListener extends PlayerKnockbackListener {
public void register() {
PlayerVelocityEvent.EVENT.register((player, velocity) -> {
DamageSource lastDamageSource = player.getLastDamageSource();
DamageSource lastDamageSource = player.getRecentDamageSource();
if (lastDamageSource == null)
return InteractionResult.PASS;
return ActionResult.PASS;
// Check if the damage is from a player attack
if (!lastDamageSource.is(DamageTypes.PLAYER_ATTACK))
return InteractionResult.PASS;
if (!lastDamageSource.isOf(DamageTypes.PLAYER_ATTACK))
return ActionResult.PASS;
onPlayerVelocity(new FabricPlayer(player), new Vector3d(velocity.x, velocity.y, velocity.z));
// This SHOULD mean we return original velocity for Fabric (no modification here)
// But since we set it ourselves due to following bukkit's design patterns and doing victim
// We return a pass so velocity isn't set twice
return InteractionResult.PASS;
return ActionResult.PASS;
});
}
}
@@ -1,13 +1,14 @@
package me.caseload.knockbacksync.listener.fabric;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.FabricBase;
import me.caseload.knockbacksync.callback.TickRateChangeEvent;
import me.caseload.knockbacksync.player.PlayerData;
public class FabricTickRateChangeListener {
public void register() {
TickRateChangeEvent.EVENT.register((oldTickRate, newTickRate) -> {
if (oldTickRate != newTickRate) {
PlayerData.TICK_RATE = newTickRate;
((FabricBase) Base.INSTANCE).setTickRate(newTickRate);
}
});
}
@@ -0,0 +1,17 @@
package me.caseload.knockbacksync.mixin;
import me.caseload.knockbacksync.entity.EntityTickManager;
import net.minecraft.entity.EntityType;
import net.minecraft.server.world.ServerChunkLoadingManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ServerChunkLoadingManager.class)
public abstract class ChunkMapMixin {
@Redirect(method = "loadEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/EntityType;getTrackTickInterval()I"))
private int getCustomUpdateInterval(EntityType<?> entityType) {
return EntityTickManager.getCustomUpdateInterval(entityType);
}
}
@@ -1,14 +0,0 @@
package me.caseload.knockbacksync.mixin;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(CommandSourceStack.class)
public interface CommandStackSourceAccessor {
@Accessor("source")
CommandSource getSource();
}
@@ -1,35 +1,34 @@
package me.caseload.knockbacksync.mixin;
import me.caseload.knockbacksync.callback.PlayerVelocityEvent;
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Player.class)
@Mixin(PlayerEntity.class)
public class PlayerMixin {
@Inject(method = "attack", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/game/ClientboundSetEntityMotionPacket;<init>(Lnet/minecraft/world/entity/Entity;)V"), cancellable = true)
@Inject(method = "attack", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityVelocityUpdateS2CPacket;<init>(Lnet/minecraft/entity/Entity;)V"), cancellable = true)
private void onAttack(Entity target, CallbackInfo ci) {
if (target instanceof ServerPlayer serverPlayer && target.hurtMarked) {
Vec3 velocity = target.getDeltaMovement();
if (target instanceof ServerPlayerEntity serverPlayer && target.velocityModified) {
Vec3d velocity = target.getVelocity();
InteractionResult result = PlayerVelocityEvent.EVENT.invoker().onVelocityChange(serverPlayer, velocity);
ActionResult result = PlayerVelocityEvent.EVENT.invoker().onVelocityChange(serverPlayer, velocity);
if (result == InteractionResult.FAIL) {
target.hurtMarked = false;
if (result == ActionResult.FAIL) {
target.velocityModified = false;
ci.cancel(); // Prevent sending the velocity packet
} else if (result == InteractionResult.SUCCESS) {
} else if (result == ActionResult.SUCCESS) {
// Currently unnecessary since we do this in the handler, will move later
target.setDeltaMovement(velocity);
serverPlayer.connection.send(new ClientboundSetEntityMotionPacket(target));
target.hurtMarked = false;
target.setVelocity(velocity);
serverPlayer.networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(target));
target.velocityModified = false;
ci.cancel(); // Prevent sending the original velocity packet
}
}
@@ -1,11 +1,11 @@
package me.caseload.knockbacksync.mixin;
import me.caseload.knockbacksync.callback.PlayerVelocityEvent;
import net.minecraft.server.level.ServerEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import net.minecraft.entity.Entity;
import net.minecraft.server.network.EntityTrackerEntry;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.Vec3d;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -13,28 +13,47 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerEntity.class)
@Mixin(EntityTrackerEntry.class)
public class ServerEntityMixin {
@Shadow
@Final
private Entity entity;
@Inject(method = "sendChanges", at = @At(value = "FIELD", target = "Lnet/minecraft/world/entity/Entity;hurtMarked:Z", ordinal = 1), cancellable = true)
// Velocity event
@Inject(method = "tick", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;velocityModified:Z", ordinal = 1), cancellable = true)
private void onSendChanges(CallbackInfo ci) {
if (this.entity.hurtMarked && this.entity instanceof ServerPlayer player) {
Vec3 velocity = player.getDeltaMovement();
if (this.entity.velocityModified && this.entity instanceof ServerPlayerEntity player) {
Vec3d velocity = player.getVelocity();
InteractionResult result = PlayerVelocityEvent.EVENT.invoker().onVelocityChange(player, velocity);
ActionResult result = PlayerVelocityEvent.EVENT.invoker().onVelocityChange(player, velocity);
if (result == InteractionResult.FAIL) {
this.entity.hurtMarked = false;
if (result == ActionResult.FAIL) {
this.entity.velocityModified = false;
ci.cancel();
} else if (result == InteractionResult.SUCCESS) {
} else if (result == ActionResult.SUCCESS) {
// Currently unnecessary since we do this in the handler, will move later
player.setDeltaMovement(velocity);
player.setVelocity(velocity);
}
}
}
// Custom tick intervals
// @Shadow
// private int updateInterval;
// @Accessor("entity")
// Entity getEntity() {
// return null;
// }
// @Invoker("sendChanges")
// void invokeSendChanges() {
//
// }
// @Override
// public void setUpdateInterval(int interval) {
// this.updateInterval = interval;
// }
}
@@ -1,21 +1,21 @@
package me.caseload.knockbacksync.mixin;
import me.caseload.knockbacksync.callback.TickRateChangeEvent;
import net.minecraft.world.TickRateManager;
import net.minecraft.world.tick.TickManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TickRateManager.class)
@Mixin(TickManager.class)
public class TickRateManagerMixin {
@Shadow
protected float tickrate;
protected float tickRate;
@Inject(method = "setTickRate", at = @At("HEAD"))
private void onSetTickRate(float tickRate, CallbackInfo ci) {
float oldTickRate = this.tickrate;
float oldTickRate = this.tickRate;
TickRateChangeEvent.EVENT.invoker().onTickRateChange(oldTickRate, tickRate);
}
}
@@ -0,0 +1,5 @@
package me.caseload.knockbacksync.mixin;
public interface UpdateIntervalAccessor {
void setUpdateInterval(int interval);
}
@@ -4,21 +4,21 @@ import me.caseload.knockbacksync.player.FabricPlayer;
import me.caseload.knockbacksync.player.PlatformPlayer;
import me.caseload.knockbacksync.sender.Sender;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.command.ServerCommandSource;
public class FabricPermissionChecker implements PermissionChecker {
public boolean hasPermission(CommandSourceStack source, String permission) {
public boolean hasPermission(ServerCommandSource source, String permission) {
return Permissions.check(source, permission, false);
}
public boolean hasPermission(CommandSourceStack source, String permission, boolean defaultIfUnset) {
public boolean hasPermission(ServerCommandSource source, String permission, boolean defaultIfUnset) {
return Permissions.check(source, permission, defaultIfUnset);
}
@Override
public boolean hasPermission(Object nativeType, String s, boolean defaultIfUnset) {
if (nativeType instanceof CommandSourceStack commandSourceStack) {
if (nativeType instanceof ServerCommandSource commandSourceStack) {
return Permissions.check(commandSourceStack, s, defaultIfUnset);
}
throw new IllegalArgumentException();
@@ -1,17 +1,26 @@
package me.caseload.knockbacksync.player;
import com.github.retrooper.packetevents.PacketEvents;
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.world.FabricWorld;
import me.caseload.knockbacksync.world.PlatformWorld;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.phys.Vec3;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.effect.StatusEffects;
import net.minecraft.registry.BuiltinRegistries;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -19,15 +28,20 @@ import java.util.Optional;
import java.util.UUID;
public class FabricPlayer implements PlatformPlayer {
public final ServerPlayer fabricPlayer;
public final ServerPlayerEntity fabricPlayer;
public final User user;
public FabricPlayer(ServerPlayer player) {
private String clientBrand = "vanilla";
public FabricPlayer(ServerPlayerEntity player) {
Preconditions.checkArgument(player != null);
this.fabricPlayer = player;
this.user = PacketEvents.getAPI().getPlayerManager().getUser(fabricPlayer);
}
@Override
public UUID getUUID() {
return fabricPlayer.getUUID();
return fabricPlayer.getUuid();
}
@Override
@@ -52,49 +66,48 @@ public class FabricPlayer implements PlatformPlayer {
@Override
public float getPitch() {
return fabricPlayer.getXRot();
return fabricPlayer.getPitch();
}
@Override
public float getYaw() {
return fabricPlayer.getYRot();
return fabricPlayer.getYaw();
}
@Override
public boolean isOnGround() {
return fabricPlayer.onGround();
return fabricPlayer.isOnGround();
}
@Override
public int getPing() {
return fabricPlayer.connection.latency();
return fabricPlayer.networkHandler.getLatency();
}
@Override
public boolean isGliding() {
return fabricPlayer.isFallFlying();
return fabricPlayer.isGliding();
}
@Override
public PlatformWorld getWorld() {
return new FabricWorld(fabricPlayer.level());
return new FabricWorld(fabricPlayer.getWorld());
}
@Override
public Vector3d getLocation() {
Vec3 pos = fabricPlayer.position();
return new Vector3d(pos.x, pos.y, pos.z);
return new Vector3d(fabricPlayer.getX(), fabricPlayer.getY(), fabricPlayer.getZ());
}
@Override
public void sendMessage(@NotNull String s) {
fabricPlayer.sendSystemMessage(Component.literal(s));
fabricPlayer.sendMessage(Text.literal(s));
}
@Override
public double getAttackCooldown() {
// this is what paper does I have no idea how this works
return fabricPlayer.getAttackStrengthScale(0.5f);
return fabricPlayer.getAttackCooldownProgress(0.5f);
}
@Override
@@ -104,26 +117,58 @@ public class FabricPlayer implements PlatformPlayer {
@Override
public int getMainHandKnockbackLevel() {
Optional<Holder.Reference<Enchantment>>
entry = VanillaRegistries.createLookup().asGetterLookup().get(
Registries.ENCHANTMENT, Enchantments.KNOCKBACK
);
Holder<Enchantment> registryEntry1 = entry.orElseThrow(); // Reference implements RegistryEntry, this is fine
return EnchantmentHelper.getItemEnchantmentLevel(registryEntry1, fabricPlayer.getMainHandItem());
RegistryEntry<Enchantment> knockbackEntry = fabricPlayer.getWorld().getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT).getOrThrow(Enchantments.KNOCKBACK);
return EnchantmentHelper.getLevel(knockbackEntry, fabricPlayer.getMainHandStack());
}
@Override
public @Nullable Integer getNoDamageTicks() {
return fabricPlayer.invulnerableTime;
return fabricPlayer.timeUntilRegen;
}
@Override
public void setVelocity(Vector3d adjustedVelocity) {
fabricPlayer.setDeltaMovement(adjustedVelocity.x, adjustedVelocity.y, adjustedVelocity.z);
fabricPlayer.setVelocity(adjustedVelocity.x, adjustedVelocity.y, adjustedVelocity.z);
// TODO
// fix paper-ism? for some reason setVelocity() in paper marks the entity as hurt marked every time its called?
fabricPlayer.hurtMarked = true;
fabricPlayer.velocityModified = true;
}
// Implement other methods
@Override
public Vector3d getVelocity() {
final Vec3d fabricVelocity = fabricPlayer.getVelocity();
return new Vector3d(fabricVelocity.x, fabricVelocity.y, fabricVelocity.z);
}
@Override
public double getJumpPower() {
double jumpVelocity = 0.42;
StatusEffectInstance jumpEffect = fabricPlayer.getStatusEffect(StatusEffects.JUMP_BOOST);
if (jumpEffect != null) {
int amplifier = jumpEffect.getAmplifier();
jumpVelocity += (amplifier + 1) * 0.1F;
}
return jumpVelocity;
}
@Override
public BoundingBox getBoundingBox() {
Box boundingBox = fabricPlayer.getBoundingBox();
return new BoundingBox(boundingBox.minX, boundingBox.minY, boundingBox.minZ, boundingBox.maxX, boundingBox.minY, boundingBox.maxZ);
}
@Override
public @Nullable User getUser() {
return user;
}
@Override
public void setClientBrand(String brand) {
this.clientBrand = brand;
}
@Override
public String getClientBrand() {
return this.clientBrand;
}
}
@@ -1,6 +1,6 @@
package me.caseload.knockbacksync.scheduler;
import me.caseload.knockbacksync.KBSyncFabricLoaderMod;
import me.caseload.knockbacksync.FabricLoaderMod;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.server.MinecraftServer;
@@ -19,10 +19,10 @@ public class FabricSchedulerAdapter implements SchedulerAdapter {
Iterator<ScheduledTask> iterator = taskMap.keySet().iterator();
while (iterator.hasNext()) {
ScheduledTask task = iterator.next();
if (server.getTickCount() >= task.nextRunTick) {
if (server.getTicks() >= task.nextRunTick) {
task.task.run();
if (task.isPeriodic) {
task.nextRunTick = server.getTickCount() + task.period;
task.nextRunTick = server.getTicks() + task.period;
} else {
iterator.remove();
}
@@ -32,7 +32,7 @@ public class FabricSchedulerAdapter implements SchedulerAdapter {
@Override
public AbstractTaskHandle runTask(Runnable task) {
ScheduledTask scheduledTask = new ScheduledTask(task, KBSyncFabricLoaderMod.getServer().getTickCount(), 0, false);
ScheduledTask scheduledTask = new ScheduledTask(task, FabricLoaderMod.getServer().getTicks(), 0, false);
Runnable cancellationTask = () -> taskMap.remove(scheduledTask);
taskMap.put(scheduledTask, cancellationTask);
return new FabricTaskHandle(cancellationTask);
@@ -52,7 +52,7 @@ public class FabricSchedulerAdapter implements SchedulerAdapter {
@Override
public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
ScheduledTask scheduledTask = new ScheduledTask(task, KBSyncFabricLoaderMod.getServer().getTickCount() + delayTicks, 0, false);
ScheduledTask scheduledTask = new ScheduledTask(task, FabricLoaderMod.getServer().getTicks() + delayTicks, 0, false);
Runnable cancellationTask = () -> taskMap.remove(scheduledTask);
taskMap.put(scheduledTask, cancellationTask);
return new FabricTaskHandle(cancellationTask);
@@ -60,7 +60,7 @@ public class FabricSchedulerAdapter implements SchedulerAdapter {
@Override
public AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks) {
ScheduledTask scheduledTask = new ScheduledTask(task, KBSyncFabricLoaderMod.getServer().getTickCount() + delayTicks, periodTicks, true);
ScheduledTask scheduledTask = new ScheduledTask(task, FabricLoaderMod.getServer().getTicks() + delayTicks, periodTicks, true);
Runnable cancellationTask = () -> taskMap.remove(scheduledTask);
taskMap.put(scheduledTask, cancellationTask);
return new FabricTaskHandle(cancellationTask);
@@ -1,42 +1,41 @@
package me.caseload.knockbacksync.sender;
import me.caseload.knockbacksync.KBSyncFabricBase;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.mixin.CommandStackSourceAccessor;
import me.caseload.knockbacksync.FabricBase;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.permission.FabricPermissionChecker;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.server.rcon.RconConsoleSource;
import net.minecraft.server.command.CommandOutput;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.rcon.RconCommandOutput;
import net.minecraft.text.Text;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.SenderMapper;
import java.util.UUID;
public class FabricSenderFactory extends SenderFactory<KBSyncFabricBase, CommandSourceStack> implements SenderMapper<CommandSourceStack, Sender> {
private final KBSyncFabricBase plugin;
public class FabricSenderFactory extends SenderFactory<FabricBase, ServerCommandSource> implements SenderMapper<ServerCommandSource, Sender> {
private final FabricBase plugin;
public FabricSenderFactory(KBSyncFabricBase kbSyncFabricBase) {
public FabricSenderFactory(FabricBase kbSyncFabricBase) {
super(kbSyncFabricBase);
this.plugin = kbSyncFabricBase;
}
@Override
protected KBSyncFabricBase getPlugin() {
protected FabricBase getPlugin() {
return this.plugin;
}
@Override
protected UUID getUniqueId(CommandSourceStack commandSource) {
protected UUID getUniqueId(ServerCommandSource commandSource) {
if (commandSource.getEntity() != null) {
return commandSource.getEntity().getUUID();
return commandSource.getEntity().getUuid();
}
return Sender.CONSOLE_UUID;
}
@Override
protected String getName(CommandSourceStack commandSource) {
String name = commandSource.getTextName();
protected String getName(ServerCommandSource commandSource) {
String name = commandSource.getName();
if (commandSource.getEntity() != null && name.equals("Server")) {
return Sender.CONSOLE_NAME;
}
@@ -44,7 +43,7 @@ public class FabricSenderFactory extends SenderFactory<KBSyncFabricBase, Command
}
@Override
protected void sendMessage(CommandSourceStack sender, String message) {
protected void sendMessage(ServerCommandSource sender, String message) {
// final Locale locale;
// if (sender.getEntity() instanceof ServerPlayer) {
// String language = ((ServerPlayer) sender.getEntity()).clientInformation().language();
@@ -53,39 +52,39 @@ public class FabricSenderFactory extends SenderFactory<KBSyncFabricBase, Command
// locale = null;
// }
// sender.sendFeedback(() -> toNativeText(TranslationManager.render(message, locale)), false);
sender.sendSuccess(() -> Component.literal(message), false);
sender.sendFeedback(() -> Text.literal(message), false);
}
@Override
protected boolean hasPermission(CommandSourceStack commandSource, String node) {
return ((FabricPermissionChecker) KnockbackSyncBase.INSTANCE.getPermissionChecker()).hasPermission(commandSource, node);
protected boolean hasPermission(ServerCommandSource commandSource, String node) {
return ((FabricPermissionChecker) Base.INSTANCE.getPermissionChecker()).hasPermission(commandSource, node);
}
@Override
protected boolean hasPermission(CommandSourceStack commandSource, String node, boolean defaultIfUnset) {
return ((FabricPermissionChecker) KnockbackSyncBase.INSTANCE.getPermissionChecker()).hasPermission(commandSource, node, defaultIfUnset);
protected boolean hasPermission(ServerCommandSource commandSource, String node, boolean defaultIfUnset) {
return ((FabricPermissionChecker) Base.INSTANCE.getPermissionChecker()).hasPermission(commandSource, node, defaultIfUnset);
}
@Override
protected void performCommand(CommandSourceStack sender, String command) {
protected void performCommand(ServerCommandSource sender, String command) {
// sender.getServer().getCommandManager().executeWithPrefix(sender, command);
}
@Override
protected boolean isConsole(CommandSourceStack sender) {
CommandSource output = ((CommandStackSourceAccessor) sender).getSource();
protected boolean isConsole(ServerCommandSource sender) {
CommandOutput output = sender.output;
return output == sender.getServer() || // Console
output.getClass() == RconConsoleSource.class || // Rcon
(output == CommandSource.NULL && sender.getTextName().equals("")); // Functions
output.getClass() == RconCommandOutput.class || // Rcon
(output == CommandOutput.DUMMY && sender.getName().equals("")); // Functions
}
@Override
public @NonNull Sender map(@NonNull CommandSourceStack base) {
public @NonNull Sender map(@NonNull ServerCommandSource base) {
return this.wrap(base);
}
@Override
public @NonNull CommandSourceStack reverse(@NonNull Sender mapped) {
public @NonNull ServerCommandSource reverse(@NonNull Sender mapped) {
return this.unwrap(mapped);
}
@@ -1,16 +1,14 @@
package me.caseload.knockbacksync.stats.custom;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.Base;
public class FabricStatsManager extends StatsManager {
public FabricStatsManager() {
super(null);
}
@Override
public void init() {
KnockbackSyncBase.INSTANCE.getScheduler().runTaskAsynchronously(() -> {
BuildTypePie.determineBuildType(); // Function to calculate hash
MetricsFabric metrics = new MetricsFabric(23568);
metrics.addCustomChart(new PlayerVersionsPie());
metrics.addCustomChart(new BuildTypePie());
super.metrics = metrics;
});
// no op remove bstats
}
}
@@ -1,151 +0,0 @@
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import me.caseload.knockbacksync.KBSyncFabricLoaderMod;
import me.caseload.knockbacksync.KnockbackSyncBase;
import me.caseload.knockbacksync.stats.CustomChart;
import me.caseload.knockbacksync.stats.JsonObjectBuilder;
import net.fabricmc.loader.api.FabricLoader;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
public class MetricsFabric implements Metrics {
private final MetricsBase metricsBase;
/**
* Creates a new Metrics instance.
*
* @param serviceId The id of the service. It can be found at <a
* href="https://bstats.org/what-is-my-plugin-id">What is my plugin id?</a>
*/
public MetricsFabric(int serviceId) {
// Get the config file
File bStatsFolder = new File(FabricLoader.getInstance().getConfigDir().toString(), "bStats");
File configFile = new File(bStatsFolder, "config.yml");
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
Config config;
try {
if (!configFile.exists()) {
bStatsFolder.mkdirs(); // Ensure the directory exists
config = new Config();
config.serverUuid = UUID.randomUUID().toString();
// Add header as a comment
// mapper.writeValue(configFile,
// "# 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.\n");
mapper.writeValue(configFile, config); // Write the config object as YAML
} else {
config = mapper.readValue(configFile, Config.class);
}
} catch (IOException ignored) {
// Handle the exception appropriately (e.g., log an error)
config = new Config(); // Fallback to default values
config.serverUuid = UUID.randomUUID().toString();
}
// Load the data
boolean enabled = config.enabled;
String serverUUID = config.serverUuid;
boolean logErrors = config.logFailedRequests;
boolean logSentData = config.logSentData;
boolean logResponseStatusText = config.logResponseStatusText;
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(
"fabric",
serverUUID,
serviceId,
enabled,
this::appendPlatformData,
this::appendServiceData,
submitDataTask -> KnockbackSyncBase.INSTANCE.getScheduler().runTask(submitDataTask),
() -> true,
(message, error) -> KnockbackSyncBase.INSTANCE.getLogger().log(Level.WARNING, message, error),
(message) -> KnockbackSyncBase.INSTANCE.getLogger().log(Level.INFO, message),
logErrors,
logSentData,
logResponseStatusText,
false);
}
/**
* Shuts down the underlying scheduler service.
*/
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", KBSyncFabricLoaderMod.getServer().usesAuthentication() ? 0 : 1);
builder.appendField("bukkitVersion", "Fabric " + FabricLoader.getInstance().getModContainer("fabricloader").get().getMetadata().getVersion().getFriendlyString() + " (MC: " + KBSyncFabricLoaderMod.getServer().getServerVersion() + ")");
builder.appendField("bukkitName", "Fabric");
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", FabricLoader.getInstance().getModContainer("knockbacksync").get().getMetadata().getVersion().getFriendlyString());
}
private int getPlayerAmount() {
if (KBSyncFabricLoaderMod.getServer().isRunning()) {
return KBSyncFabricLoaderMod.getServer().getPlayerCount();
} else {
return 0;
}
}
private static class Config {
public boolean enabled = true;
public String serverUuid;
public boolean logFailedRequests = false;
public boolean logSentData = false;
public boolean logResponseStatusText = false;
}
}
@@ -1,46 +1,45 @@
package me.caseload.knockbacksync.util;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import java.util.function.Supplier;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
public class CommandUtil {
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, String message) {
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, String message) {
sendSuccessMessage(context, () -> getComponent(message), false);
}
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, String message, boolean allowLogging) {
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, String message, boolean allowLogging) {
sendSuccessMessage(context, () -> getComponent(message), allowLogging);
}
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, Component message) {
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, Text message) {
sendSuccessMessage(context, () -> message, false);
}
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, Component message, boolean allowLogging) {
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, Text message, boolean allowLogging) {
sendSuccessMessage(context, () -> message, allowLogging);
}
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, Supplier<Component> messageSupplier) {
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, Supplier<Text> messageSupplier) {
sendSuccessMessage(context, messageSupplier, false);
}
public static void sendSuccessMessage(CommandContext<CommandSourceStack> context, Supplier<Component> messageSupplier, boolean allowLogging) {
context.getSource().sendSuccess(messageSupplier, allowLogging);
public static void sendSuccessMessage(CommandContext<ServerCommandSource> context, Supplier<Text> messageSupplier, boolean allowLogging) {
context.getSource().sendFeedback(messageSupplier, allowLogging);
}
public static void sendFailureMessage(CommandContext<CommandSourceStack> context, String message) {
public static void sendFailureMessage(CommandContext<ServerCommandSource> context, String message) {
sendFailureMessage(context, getComponent(message));
}
public static void sendFailureMessage(CommandContext<CommandSourceStack> context, Component message) {
context.getSource().sendFailure(message);
public static void sendFailureMessage(CommandContext<ServerCommandSource> context, Text message) {
context.getSource().sendError(message);
}
public static Component getComponent(String message) {
return Component.literal(message);
public static Text getComponent(String message) {
return Text.literal(message);
}
}
@@ -0,0 +1,56 @@
package me.caseload.knockbacksync.util;
import me.caseload.knockbacksync.Base;
import me.caseload.knockbacksync.ConfigWrapper;
import me.caseload.knockbacksync.mixin.UpdateIntervalAccessor;
import net.minecraft.entity.Entity;
import net.minecraft.server.world.ServerChunkLoadingManager;
import net.minecraft.server.world.ServerChunkManager;
import net.minecraft.server.world.ServerWorld;
import java.util.HashMap;
import java.util.Map;
public class CustomUpdateIntervals {
private static final Map<String, Integer> entityIntervals = new HashMap<>();
public static void setInterval(String entityType, int interval) {
entityIntervals.put(entityType, interval);
}
public static int getInterval(String entityType, int defaultInterval) {
return entityIntervals.getOrDefault(entityType, defaultInterval);
}
public static void clear() {
entityIntervals.clear();
}
public static void updateIntervals(ServerWorld level, Map<String, Integer> entityIntervals) {
for (Entity entity : level.iterateEntities()) {
String entityType = entity.getType().getRegistryEntry().getIdAsString();
if (entityIntervals.containsKey(entityType)) {
ServerChunkLoadingManager.EntityTracker serverEntity = ((ServerChunkManager) entity.getWorld().getChunkManager()).chunkLoadingManager.entityTrackers.get(entity.getId());
if (serverEntity != null) {
((UpdateIntervalAccessor) serverEntity).setUpdateInterval(entityIntervals.get(entityType));
}
}
}
}
public static Map<String, Integer> loadEntityIntervalsFromConfig() {
Map<String, Integer> intervals = new HashMap<>();
ConfigWrapper config = Base.INSTANCE.getConfigManager().getConfigWrapper();
String basePath = "entity_update_intervals";
if (config.contains(basePath)) {
for (String entityType : config.getKeys(basePath)) {
int interval = config.getInt(basePath + "." + entityType, -1);
if (interval > 0) {
intervals.put(entityType, interval);
}
}
}
return intervals;
}
}
@@ -1,23 +1,27 @@
package me.caseload.knockbacksync.world;
import me.caseload.knockbacksync.KBSyncFabricLoaderMod;
import me.caseload.knockbacksync.FabricLoaderMod;
import me.caseload.knockbacksync.player.FabricPlayer;
import me.caseload.knockbacksync.player.PlatformPlayer;
import net.minecraft.server.network.ServerPlayerEntity;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
public class FabricServer implements PlatformServer {
public Collection<PlatformPlayer> getOnlinePlayers() {
return KBSyncFabricLoaderMod.getServer().getPlayerList().getPlayers().stream()
return FabricLoaderMod.getServer().getPlayerManager().getPlayerList().stream()
.map(FabricPlayer::new)
.collect(Collectors.toList());
}
@Override
public PlatformPlayer getPlayer(UUID uuid) {
return new FabricPlayer(KBSyncFabricLoaderMod.getServer().getPlayerList().getPlayer(uuid));
return new FabricPlayer(FabricLoaderMod.getServer().getPlayerManager().getPlayer(uuid));
}
@Override
public PlatformPlayer getPlayer(Object nativePlatformPlayer) {
return new FabricPlayer((ServerPlayerEntity) nativePlatformPlayer);
}
}
@@ -6,26 +6,27 @@ import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import me.caseload.knockbacksync.world.raytrace.FluidHandling;
import me.caseload.knockbacksync.world.raytrace.RayTraceResult;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.ShapeContext;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.HitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.RaycastContext;
import net.minecraft.world.World;
public class FabricWorld implements PlatformWorld {
private final Level world;
private final World world;
public FabricWorld(Level world) {
public FabricWorld(World world) {
this.world = world;
}
@Override
public WrappedBlockState getBlockStateAt(int x, int y, int z) {
BlockState blockState = this.world.getBlockState(new BlockPos(x, y, z));
return WrappedBlockState.getByString(blockState.getBlock().getName().toString());
return WrappedBlockState.getByGlobalId(Block.STATE_IDS.getRawId(blockState));
}
@Override
@@ -35,27 +36,28 @@ public class FabricWorld implements PlatformWorld {
@Override
public RayTraceResult rayTraceBlocks(Vector3d start, Vector3d direction, double maxDistance, FluidHandling fluidHandling, boolean ignorePassableBlocks) {
Vec3 startVec = new Vec3(start.getX(), start.getY(), start.getZ());
Vec3 endVec = startVec.add(direction.getX() * maxDistance, direction.getY() * maxDistance, direction.getZ() * maxDistance);
Vec3d startVec = new Vec3d(start.getX(), start.getY(), start.getZ());
Vec3d endVec = startVec.add(direction.getX() * maxDistance, direction.getY() * maxDistance, direction.getZ() * maxDistance);
BlockHitResult result = world.clip(new ClipContext(
BlockHitResult result = world.raycast(new RaycastContext(
startVec,
endVec,
ClipContext.Block.OUTLINE,
fluidHandling == FluidHandling.NONE ? ClipContext.Fluid.NONE :
fluidHandling == FluidHandling.SOURCE_ONLY ? ClipContext.Fluid.SOURCE_ONLY :
ClipContext.Fluid.ANY,
(Entity) null
RaycastContext.ShapeType.OUTLINE,
fluidHandling == FluidHandling.NONE ? RaycastContext.FluidHandling.NONE :
fluidHandling == FluidHandling.SOURCE_ONLY ? RaycastContext.FluidHandling.SOURCE_ONLY :
RaycastContext.FluidHandling.ANY,
ShapeContext.absent()
));
if (result.getType() == HitResult.Type.MISS) return null;
Vec3d hitLocation = result.getPos();
BlockPos blockPos = result.getBlockPos();
return new RayTraceResult(
new Vector3d(result.getLocation().x, result.getLocation().y, result.getLocation().z),
BlockFace.getBlockFaceByValue(result.getDirection().ordinal()),
new Vector3i(result.getBlockPos().getX(), result.getBlockPos().getY(), result.getBlockPos().getZ()),
WrappedBlockState.getByString(world.getBlockState(blockPos).getBlock().toString())
new Vector3d(hitLocation.x, hitLocation.y, hitLocation.z),
BlockFace.getBlockFaceByValue(result.getSide().ordinal()),
new Vector3i(blockPos.getX(), blockPos.getY(), blockPos.getZ()),
getBlockStateAt(blockPos.getX(), blockPos.getY(), blockPos.getZ())
);
}
}
+9 -4
View File
@@ -10,16 +10,21 @@
"icon": "assets/knockbacksync/icon.png",
"environment": "*",
"entrypoints": {
"preLaunch": [
"me.caseload.knockbacksync.FabricLoaderMod"
],
"main": [
"me.caseload.knockbacksync.KBSyncFabricLoaderMod"
"me.caseload.knockbacksync.FabricLoaderMod"
]
},
"mixins": [
"knockbacksync.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabricloader": ">=0.15.11",
"fabric": "*",
"minecraft": "~${minecraft_version}"
}
"minecraft": "~${minecraft_version}",
"packetevents": "*"
},
"accessWidener": "knockbacksync.accesswidener"
}
@@ -0,0 +1,6 @@
accessWidener v2 named
accessible field net/minecraft/server/command/ServerCommandSource output Lnet/minecraft/server/command/CommandOutput;
accessible field net/minecraft/server/world/ServerChunkLoadingManager entityTrackers Lit/unimi/dsi/fastutil/ints/Int2ObjectMap;
accessible class net/minecraft/server/world/ServerChunkLoadingManager$EntityTracker
#accessible method net/minecraft/server/level/ChunkMap$TrackedEntity updatePlayers (Ljava/util/List;)V
@@ -4,7 +4,7 @@
"package": "me.caseload.knockbacksync.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"CommandStackSourceAccessor",
"ChunkMapMixin",
"PlayerMixin",
"ServerEntityMixin",
"TickRateManagerMixin"
+6 -6
View File
@@ -4,11 +4,9 @@ org.gradle.parallel=true
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.21
parchment_mappings=2024.07.28
parchment_minecraft_version=1.21
yarn_mappings=1.21+build.9
loader_version=0.15.11
minecraft_version=1.21.5
yarn_mappings=1.21.5+build.1:v2
loader_version=0.16.13
# Mod Properties
maven_group=me.caseload
@@ -16,4 +14,6 @@ archives_base_name=knockbacksync
# Dependencies
# check this on https://modmuss50.me/fabric.html
fabric_version=0.102.0+1.21
fabric_version=0.121.0+1.21.5
githubRepo=CASELOAD7000/knockback-sync
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-136
View File
@@ -1,136 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.caseload</groupId>
<artifactId>KnockbackSync</artifactId>
<version>1.3.2</version>
<packaging>jar</packaging>
<name>KnockbackSync</name>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--outputTimestamp must be set to a consistent arbitrary value for reproducibility-->
<!--Chosen value is the exact time caseload uploaded the first commit-->
<project.build.outputTimestamp>2024-01-18T02:53:12Z</project.build.outputTimestamp>
</properties>
<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<minimizeJar>true</minimizeJar>
<relocations>
<relocation>
<pattern>dev.jorel.commandapi</pattern>
<shadedPattern>me.caseload.knockbacksync.commandapi</shadedPattern>
</relocation>
<relocation>
<pattern>com.github.retrooper</pattern>
<shadedPattern>me.caseload.knockbacksync.retrooper</shadedPattern>
</relocation>
<relocation>
<pattern>org.projectlombok</pattern>
<shadedPattern>me.caseload.knockbacksync.projectlombok</shadedPattern>
</relocation>
<relocation>
<pattern>org.kohsuke</pattern>
<shadedPattern>me.caseload.knockbacksync.kohsuke</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>me.caseload.knockbacksync.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<repositories>
<repository>
<id>spigotmc-repo</id> <!--spigot-api-->
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository> <!--packetevents-spigot-->
<id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-releases/</url>
</repository>
<repository> <!--Backup repo for maven central, mainly for geysermc api, bstats-bukkit-->
<id>opencollab</id>
<url>https://repo.opencollab.dev/maven-snapshots/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.retrooper</groupId>
<artifactId>packetevents-spigot</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>dev.jorel</groupId>
<artifactId>commandapi-bukkit-shade</artifactId>
<version>9.5.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.326</version>
</dependency>
<dependency>
<groupId>org.geysermc.floodgate</groupId>
<artifactId>api</artifactId>
<version>2.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,8 @@
package me.caseload.knockbacksync.listener;
import org.bukkit.event.Listener;
public class PlayerMoveListener implements Listener {
}