timeline = new HashMap<>();
43 |
44 | @NotNull
45 | private final Random random = new Random();
46 |
47 | @Nullable
48 | private AbstractTaskHandle combatTask;
49 |
50 | @Nullable @Setter
51 | private Long ping, previousPing;
52 |
53 | @Nullable @Setter
54 | private Double verticalVelocity;
55 |
56 | @Nullable @Setter
57 | private Integer lastDamageTicks;
58 |
59 | @Setter
60 | private double gravity = 0.08;
61 |
62 | public PlayerData(Player player) {
63 | this.player = player;
64 | this.user = PacketEvents.getAPI().getPlayerManager().getUser(player);
65 | PING_OFFSET = KnockbackSync.getInstance().getConfig().getInt("ping_offset", 25);
66 | }
67 |
68 | /**
69 | * Calculates the player's ping with compensation for lag spikes.
70 | * A hardcoded offset is applied for several reasons,
71 | * read the GitHub FAQ before adjusting.
72 | *
73 | * @return The compensated ping, with a minimum of 1.
74 | */
75 | public long getEstimatedPing() {
76 | long currentPing = (ping != null) ? ping : player.getPing();
77 | long lastPing = (previousPing != null) ? previousPing : player.getPing();
78 | long ping = (currentPing - lastPing > KnockbackSync.getInstance().getConfigManager().getSpikeThreshold()) ? lastPing : currentPing;
79 |
80 | return Math.max(1, ping - PING_OFFSET);
81 | }
82 |
83 | public void sendPing() {
84 | int packetId = random.nextInt(1, 10000);
85 |
86 | timeline.put(packetId, System.currentTimeMillis());
87 |
88 | WrapperPlayServerPing packet = new WrapperPlayServerPing(packetId);
89 | PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet);
90 | }
91 |
92 | /**
93 | * Determines if the Player is on the ground clientside, but not serverside
94 | *
95 | * Returns ping ≥ (tMax + tFall)
and gDist ≤ 1.3
96 | *
97 | * Where:
98 | *
99 | * ping
: Estimated latency
100 | * tMax
: Time to reach maximum upward velocity
101 | * tFall
: Time to fall to the ground
102 | * gDist
: Distance to the ground
103 | *
104 | *
105 | * @param verticalVelocity The Player's current vertical velocity.
106 | * @return true
if the Player is on the ground; false
otherwise.
107 | */
108 | public boolean isOnGround(double verticalVelocity) {
109 | Material material = player.getLocation().getBlock().getType();
110 | if (player.isGliding() || material == Material.WATER || material == Material.LAVA
111 | || material == Material.COBWEB || material == Material.SCAFFOLDING)
112 | return false;
113 |
114 | if (ping == null || ping < PING_OFFSET)
115 | return false;
116 |
117 | double gDist = getDistanceToGround();
118 | if (gDist <= 0)
119 | return false; // prevent player from taking adjusted knockback when on ground serverside
120 |
121 | int tMax = verticalVelocity > 0 ? MathUtil.calculateTimeToMaxVelocity(verticalVelocity, gravity) : 0;
122 | double mH = verticalVelocity > 0 ? MathUtil.calculateDistanceTraveled(verticalVelocity, tMax, gravity) : 0;
123 | int tFall = MathUtil.calculateFallTime(verticalVelocity, mH + gDist, gravity);
124 |
125 | if (tFall == -1)
126 | return false; // reached the max tick limit, not safe to predict
127 |
128 | return getEstimatedPing() >= tMax + tFall / 20.0 * 1000 && gDist <= 1.3;
129 | }
130 |
131 | /**
132 | * Ray traces from each corner of the player's bounding box to the ground,
133 | * returning the smallest distance, with a maximum limit of 5 blocks.
134 | *
135 | * @return The distance to the ground in blocks
136 | */
137 | public double getDistanceToGround() {
138 | double collisionDist = 5;
139 |
140 | World world = player.getWorld();
141 |
142 | for (Location corner : getBBCorners()) {
143 | RayTraceResult result = world.rayTraceBlocks(corner, new Vector(0, -1, 0), 5, FluidCollisionMode.NEVER, true);
144 | if (result == null || result.getHitBlock() == null)
145 | continue;
146 |
147 | collisionDist = Math.min(collisionDist, corner.getY() - result.getHitBlock().getY());
148 | }
149 |
150 | return collisionDist - 1;
151 | }
152 |
153 | /**
154 | * Gets the corners of the Player's bounding box.
155 | *
156 | * @return An array of locations representing the corners of the bounding box.
157 | */
158 | public Location @NotNull [] getBBCorners() {
159 | BoundingBox boundingBox = player.getBoundingBox();
160 | Location location = player.getLocation();
161 | World world = location.getWorld();
162 |
163 | double adjustment = 0.01; // To ensure the bounding box isn't clipping inside a wall
164 |
165 | return new Location[] {
166 | new Location(world, boundingBox.getMinX() + adjustment, location.getY(), boundingBox.getMinZ() + adjustment),
167 | new Location(world, boundingBox.getMinX() + adjustment, location.getY(), boundingBox.getMaxZ() - adjustment),
168 | new Location(world, boundingBox.getMaxX() - adjustment, location.getY(), boundingBox.getMinZ() + adjustment),
169 | new Location(world, boundingBox.getMaxX() - adjustment, location.getY(), boundingBox.getMaxZ() - adjustment)
170 | };
171 | }
172 |
173 | /**
174 | * Calculates the positive vertical velocity.
175 | * This is used to switch falling knockback to rising knockback.
176 | *
177 | * @param attacker The player who is attacking.
178 | * @return The calculated positive vertical velocity, consistent with vanilla behavior.
179 | */
180 | public double calculateVerticalVelocity(Player attacker) {
181 | double yAxis = attacker.getAttackCooldown() > 0.848 ? 0.4 : 0.36080000519752503;
182 |
183 | if (!attacker.isSprinting()) {
184 | yAxis = 0.36080000519752503;
185 | double knockbackResistance = player.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue();
186 | double resistanceFactor = 0.04000000119 * knockbackResistance * 10;
187 | yAxis -= resistanceFactor;
188 | }
189 |
190 | // vertical velocity is always 0.4 when you have knockback level higher than 0
191 | if (attacker.getInventory().getItemInMainHand().getEnchantmentLevel(Enchantment.KNOCKBACK) > 0)
192 | yAxis = 0.4;
193 |
194 | return yAxis;
195 | }
196 |
197 | // might need soon
198 | public double calculateJumpVelocity() {
199 | double jumpVelocity = 0.42;
200 |
201 | PotionEffect jumpEffect = player.getPotionEffect(PotionEffectType.JUMP);
202 | if (jumpEffect != null) {
203 | int amplifier = jumpEffect.getAmplifier();
204 | jumpVelocity += (amplifier + 1) * 0.1F;
205 | }
206 |
207 | return jumpVelocity;
208 | }
209 |
210 | public boolean isInCombat() {
211 | return combatTask != null;
212 | }
213 |
214 | public void updateCombat() {
215 | if (isInCombat())
216 | combatTask.cancel();
217 |
218 | combatTask = newCombatTask();
219 | CombatManager.addPlayer(player.getUniqueId());
220 | }
221 |
222 | public void quitCombat(boolean cancelTask) {
223 | if (cancelTask)
224 | combatTask.cancel();
225 |
226 | combatTask = null;
227 | CombatManager.removePlayer(player.getUniqueId());
228 | timeline.clear(); // failsafe for packet loss idk
229 | }
230 |
231 | @NotNull
232 | private AbstractTaskHandle newCombatTask() {
233 | return KnockbackSync.INSTANCE.getScheduler().runTaskLaterAsynchronously(
234 | () -> quitCombat(false), KnockbackSync.getInstance().getConfigManager().getCombatTimer());
235 | }
236 |
237 | public ClientVersion getClientVersion() {
238 | ClientVersion ver = user.getClientVersion();
239 | if (ver == null) {
240 | // If temporarily null, assume server version...
241 | return ClientVersion.getById(PacketEvents.getAPI().getServerManager().getVersion().getProtocolVersion());
242 | }
243 | return ver;
244 | }
245 | }
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/manager/PlayerDataManager.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.manager;
2 |
3 | import io.github.retrooper.packetevents.util.GeyserUtil;
4 | import me.caseload.knockbacksync.util.FloodgateUtil;
5 | import org.bukkit.Bukkit;
6 | import org.bukkit.entity.Player;
7 | import org.jetbrains.annotations.NotNull;
8 |
9 | import java.util.*;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | public class PlayerDataManager {
13 |
14 | private static final Map playerDataMap = new ConcurrentHashMap<>();
15 |
16 | public static PlayerData getPlayerData(@NotNull UUID uuid) {
17 | return playerDataMap.get(uuid);
18 | }
19 |
20 | public static void addPlayerData(@NotNull UUID uuid, @NotNull PlayerData playerData) {
21 | if (!shouldExempt(uuid))
22 | playerDataMap.put(uuid, playerData);
23 | }
24 |
25 | public static void removePlayerData(@NotNull UUID uuid) {
26 | playerDataMap.remove(uuid);
27 | }
28 |
29 | public static boolean containsPlayerData(@NotNull UUID uuid) {
30 | return playerDataMap.containsKey(uuid);
31 | }
32 |
33 | public static boolean shouldExempt(@NotNull UUID uuid) {
34 | // Geyser players don't have Java movement
35 | return GeyserUtil.isGeyserPlayer(uuid)
36 | // Floodgate is the authentication system for Geyser on servers that use Geyser as a proxy instead of installing it as a plugin directly on the server
37 | || FloodgateUtil.isFloodgatePlayer(uuid)
38 | // Geyser formatted player string
39 | // This will never happen for Java players, as the first character in the 3rd group is always 4 (xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx)
40 | || uuid.toString().startsWith("00000000-0000-0000-0009");
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/runnable/PingRunnable.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.runnable;
2 |
3 | import me.caseload.knockbacksync.KnockbackSync;
4 | import me.caseload.knockbacksync.manager.CombatManager;
5 | import me.caseload.knockbacksync.manager.PlayerData;
6 | import me.caseload.knockbacksync.manager.PlayerDataManager;
7 | import org.bukkit.scheduler.BukkitRunnable;
8 |
9 | import java.util.UUID;
10 |
11 | public class PingRunnable implements Runnable {
12 |
13 | @Override
14 | public void run() {
15 | if (!KnockbackSync.getInstance().getConfigManager().isToggled())
16 | return;
17 |
18 | for (UUID uuid : CombatManager.getPlayers()) {
19 | PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
20 | playerData.sendPing();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/scheduler/AbstractTaskHandle.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.scheduler;
2 |
3 | import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
4 | import org.bukkit.plugin.Plugin;
5 | import org.bukkit.scheduler.BukkitTask;
6 | import org.jetbrains.annotations.NotNull;
7 |
8 | public class AbstractTaskHandle {
9 | private BukkitTask bukkitTask;
10 | private ScheduledTask scheduledTask;
11 |
12 | public AbstractTaskHandle(@NotNull BukkitTask bukkitTask) {
13 | this.bukkitTask = bukkitTask;
14 | }
15 |
16 | public AbstractTaskHandle(@NotNull ScheduledTask scheduledTask) {
17 | this.scheduledTask = scheduledTask;
18 | }
19 |
20 | public Plugin getOwner() {
21 | return this.bukkitTask != null ? this.bukkitTask.getOwner() : this.scheduledTask.getOwningPlugin();
22 | }
23 |
24 | public boolean isCancelled() {
25 | return this.bukkitTask != null ? this.bukkitTask.isCancelled() : this.scheduledTask.isCancelled();
26 | }
27 |
28 | public void cancel() {
29 | if (this.bukkitTask != null) {
30 | this.bukkitTask.cancel();
31 | } else {
32 | this.scheduledTask.cancel();
33 | }
34 |
35 | }
36 | }
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/scheduler/BukkitSchedulerAdapter.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.scheduler;
2 |
3 | import org.bukkit.Bukkit;
4 | import org.bukkit.plugin.Plugin;
5 | import org.bukkit.scheduler.BukkitScheduler;
6 |
7 | public class BukkitSchedulerAdapter implements SchedulerAdapter {
8 | private final Plugin plugin;
9 | private final BukkitScheduler scheduler;
10 |
11 | public BukkitSchedulerAdapter(Plugin plugin) {
12 | this.plugin = plugin;
13 | this.scheduler = Bukkit.getScheduler();
14 | }
15 |
16 | @Override
17 | public AbstractTaskHandle runTask(Runnable task) {
18 | return new AbstractTaskHandle(scheduler.runTask(plugin, task));
19 | }
20 |
21 | @Override
22 | public AbstractTaskHandle runTaskAsynchronously(Runnable task) {
23 | return new AbstractTaskHandle(scheduler.runTaskAsynchronously(plugin, task));
24 | }
25 |
26 | @Override
27 | public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
28 | return new AbstractTaskHandle(scheduler.runTaskLater(plugin, task, delayTicks));
29 | }
30 |
31 | @Override
32 | public AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks) {
33 | return new AbstractTaskHandle(scheduler.runTaskTimer(plugin, task, delayTicks, periodTicks));
34 | }
35 |
36 | @Override
37 | public AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay) {
38 | return new AbstractTaskHandle(scheduler.runTaskLaterAsynchronously(plugin, task, delay));
39 | }
40 |
41 | @Override
42 | public AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period) {
43 | return new AbstractTaskHandle(scheduler.runTaskTimerAsynchronously(plugin, task, delay, period));
44 | }
45 | }
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/scheduler/FoliaSchedulerAdapter.java:
--------------------------------------------------------------------------------
1 | // FoliaSchedulerAdapter.java
2 | package me.caseload.knockbacksync.scheduler;
3 |
4 | import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler;
5 | import org.bukkit.Bukkit;
6 | import org.bukkit.plugin.Plugin;
7 |
8 | import java.lang.reflect.InvocationTargetException;
9 | import java.lang.reflect.Method;
10 |
11 | public class FoliaSchedulerAdapter implements SchedulerAdapter {
12 | private final Plugin plugin;
13 | private GlobalRegionScheduler scheduler = null;
14 |
15 | public FoliaSchedulerAdapter(Plugin plugin) {
16 | this.plugin = plugin;
17 | try {
18 | // Attempt to find and call the `getGlobalRegionScheduler` method
19 | Method getSchedulerMethod = Bukkit.getServer().getClass().getMethod("getGlobalRegionScheduler");
20 | scheduler = (GlobalRegionScheduler) getSchedulerMethod.invoke(Bukkit.getServer());
21 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
22 | plugin.getLogger().severe("Failed to access GlobalRegionScheduler: " + e.getMessage());
23 | }
24 | }
25 |
26 | @Override
27 | public AbstractTaskHandle runTask(Runnable task) {
28 | // scheduler.execute(plugin, task);
29 | return new AbstractTaskHandle(scheduler.run(plugin, scheduledTask -> task.run()));
30 | }
31 |
32 | @Override
33 | public AbstractTaskHandle runTaskAsynchronously(Runnable task) {
34 | return new AbstractTaskHandle(scheduler.run(plugin, scheduledTask -> task.run()));
35 | }
36 |
37 | @Override
38 | public AbstractTaskHandle runTaskLater(Runnable task, long delayTicks) {
39 | return new AbstractTaskHandle(scheduler.runDelayed(plugin, scheduledTask -> task.run(), delayTicks));
40 | }
41 |
42 | @Override
43 | public AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks) {
44 | return new AbstractTaskHandle(scheduler.runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks));
45 | }
46 |
47 | @Override
48 | public AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay) {
49 | return new AbstractTaskHandle(scheduler.runDelayed(plugin, scheduledTask -> task.run(), delay));
50 | }
51 |
52 | @Override
53 | public AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period) {
54 | return new AbstractTaskHandle(scheduler.runAtFixedRate(plugin, scheduledTask -> task.run(), delay, period));
55 | }
56 | }
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/scheduler/SchedulerAdapter.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.scheduler;
2 |
3 | public interface SchedulerAdapter {
4 | AbstractTaskHandle runTask(Runnable task);
5 | AbstractTaskHandle runTaskAsynchronously(Runnable task);
6 | AbstractTaskHandle runTaskLater(Runnable task, long delayTicks);
7 | AbstractTaskHandle runTaskTimer(Runnable task, long delayTicks, long periodTicks);
8 | AbstractTaskHandle runTaskLaterAsynchronously(Runnable task, long delay);
9 | AbstractTaskHandle runTaskTimerAsynchronously(Runnable task, long delay, long period);
10 | }
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/stats/BuildTypePie.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.stats;
2 |
3 | import com.google.gson.JsonObject;
4 | import com.google.gson.JsonParser;
5 | import me.caseload.knockbacksync.KnockbackSync;
6 | import org.bstats.charts.SimplePie;
7 | import org.bukkit.Bukkit;
8 | import org.kohsuke.github.GHAsset;
9 | import org.kohsuke.github.GHRelease;
10 | import org.kohsuke.github.GitHub;
11 |
12 | import java.io.File;
13 | import java.io.FileOutputStream;
14 | import java.io.IOException;
15 | import java.io.InputStream;
16 | import java.net.URL;
17 | import java.nio.charset.StandardCharsets;
18 | import java.nio.file.Files;
19 | import java.nio.file.Paths;
20 | import java.security.MessageDigest;
21 | import java.util.List;
22 |
23 | public class BuildTypePie extends SimplePie {
24 |
25 | private static final String RELEASES_FILE = "releases.txt";
26 | private static final String DEV_BUILDS_FILE = "dev-builds.txt";
27 | private static final File dataFolder = KnockbackSync.INSTANCE.getDataFolder();
28 | private static String cachedBuildType = null;
29 |
30 | public BuildTypePie() {
31 | super("build_type", BuildTypePie::determineBuildType);
32 | }
33 |
34 | public static String determineBuildType() {
35 | if (cachedBuildType == null) {
36 | cachedBuildType = calculateBuildType();
37 | }
38 | return cachedBuildType;
39 | }
40 |
41 | private static String calculateBuildType() {
42 | try {
43 | String currentHash = getPluginJarHash();
44 | downloadBuildFiles();
45 |
46 | if (isHashInFile(currentHash, new File(dataFolder, RELEASES_FILE))) {
47 | return "release";
48 | } else if (isHashInFile(currentHash, new File(dataFolder, DEV_BUILDS_FILE))) {
49 | return "dev";
50 | } else {
51 | return "fork";
52 | }
53 | } catch (Exception e) {
54 | e.printStackTrace();
55 | return "unknown";
56 | }
57 | }
58 |
59 | private static void downloadBuildFiles() throws IOException {
60 | GitHub gitHub = GitHub.connectAnonymously();
61 | GHRelease latestRelease = gitHub.getRepository("Axionize/knockback-sync")
62 | .getLatestRelease();
63 | List assets = latestRelease.listAssets().toList();
64 | for (GHAsset asset : assets) {
65 | if (asset.getName().equals(RELEASES_FILE) || asset.getName().equals(DEV_BUILDS_FILE)) {
66 | KnockbackSync.INSTANCE.getLogger().info("Downloading: " + asset.getName());
67 |
68 | String jsonContent = readStringFromURL(asset.getUrl().toString());
69 | JsonObject jsonObject = JsonParser.parseString(jsonContent).getAsJsonObject();
70 | String downloadUrl = jsonObject.get("browser_download_url").getAsString();
71 |
72 | try (InputStream inputStream = new URL(downloadUrl).openStream();
73 | FileOutputStream outputStream = new FileOutputStream(new File(dataFolder, asset.getName()))) {
74 | inputStream.transferTo(outputStream);
75 | }
76 |
77 | KnockbackSync.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
78 | }
79 | }
80 | }
81 |
82 | private static boolean isHashInFile(String hash, File file) throws IOException {
83 | if (!file.exists()) {
84 | return false;
85 | }
86 | List lines = Files.readAllLines(Paths.get(file.getPath()));
87 | return lines.contains(hash);
88 | }
89 |
90 | private static String getPluginJarHash() throws Exception {
91 | URL jarUrl = Bukkit.getPluginManager().getPlugin("KnockbackSync").getClass().getProtectionDomain().getCodeSource().getLocation();
92 | MessageDigest digest = MessageDigest.getInstance("SHA-256");
93 | try (InputStream is = jarUrl.openStream()) {
94 | byte[] buffer = new byte[8192];
95 | int read;
96 | while ((read = is.read(buffer)) > 0) {
97 | digest.update(buffer, 0, read);
98 | }
99 | }
100 | byte[] hash = digest.digest();
101 | StringBuilder hexString = new StringBuilder();
102 | for (byte b : hash) {
103 | String hex = Integer.toHexString(0xff & b);
104 | if (hex.length() == 1) hexString.append('0');
105 | hexString.append(hex);
106 | }
107 | return hexString.toString();
108 | }
109 |
110 | private static String readStringFromURL(String urlString) throws IOException {
111 | try (InputStream inputStream = new URL(urlString).openStream()) {
112 | return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/stats/PlayerVersionsPie.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.stats;
2 |
3 | import me.caseload.knockbacksync.manager.PlayerData;
4 | import me.caseload.knockbacksync.manager.PlayerDataManager;
5 | import org.bstats.charts.AdvancedPie;
6 | import org.bukkit.Bukkit;
7 | import org.bukkit.entity.Player;
8 |
9 | import java.util.HashMap;
10 | import java.util.Map;
11 |
12 | public class PlayerVersionsPie extends AdvancedPie {
13 |
14 | // Gets the client versions of players online
15 | public PlayerVersionsPie() {
16 | super("player_version", () -> {
17 | Map valueMap = new HashMap<>();
18 | for (Player player : Bukkit.getOnlinePlayers()) {
19 | PlayerData playerData = PlayerDataManager.getPlayerData(player.getUniqueId());
20 | valueMap.put(playerData.getClientVersion().toString(), valueMap.getOrDefault(playerData.getClientVersion().toString(), 0) + 1);
21 | }
22 | return valueMap;
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/stats/StatsManager.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.stats;
2 |
3 | import me.caseload.knockbacksync.KnockbackSync;
4 | import org.bstats.bukkit.Metrics;
5 | import org.bukkit.Bukkit;
6 |
7 | public class StatsManager {
8 |
9 | public static Metrics metrics;
10 |
11 | public static void init() {
12 | KnockbackSync.INSTANCE.getScheduler().runTaskAsynchronously(() -> {
13 | BuildTypePie.determineBuildType(); // Function to calculate hash
14 | metrics = new Metrics(KnockbackSync.INSTANCE, 23568);
15 | metrics.addCustomChart(new PlayerVersionsPie());
16 | metrics.addCustomChart(new BuildTypePie());
17 | });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/util/FloodgateUtil.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.util;
2 |
3 | import org.geysermc.floodgate.api.FloodgateApi;
4 |
5 | import java.util.UUID;
6 |
7 | public class FloodgateUtil {
8 |
9 | private static boolean CHECKED_FOR_FLOODGATE;
10 | private static boolean FLOODGATE_PRESENT;
11 |
12 | public static boolean isFloodgatePlayer(UUID uuid) {
13 | if (!CHECKED_FOR_FLOODGATE) {
14 | try {
15 | Class.forName("org.geysermc.floodgate.api.FloodgateApi");
16 | FLOODGATE_PRESENT = true;
17 | } catch (ClassNotFoundException e) {
18 | FLOODGATE_PRESENT = false;
19 | }
20 | CHECKED_FOR_FLOODGATE = true;
21 | }
22 |
23 | if (FLOODGATE_PRESENT) {
24 | return FloodgateApi.getInstance().isFloodgatePlayer(uuid);
25 | } else {
26 | return false;
27 | }
28 | }
29 |
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/main/java/me/caseload/knockbacksync/util/MathUtil.java:
--------------------------------------------------------------------------------
1 | package me.caseload.knockbacksync.util;
2 |
3 | public class MathUtil {
4 |
5 | private static final double TERMINAL_VELOCITY = 3.92;
6 | private static final double MULTIPLIER = 0.98;
7 | private static final int MAX_TICKS = 20;
8 |
9 | public static double calculateDistanceTraveled(double velocity, int time, double acceleration)
10 | {
11 | double totalDistance = 0;
12 |
13 | for (int i = 0; i < time; i++)
14 | {
15 | totalDistance += velocity;
16 | velocity = ((velocity - acceleration) * MULTIPLIER);
17 | velocity = Math.min(velocity, TERMINAL_VELOCITY);
18 | }
19 |
20 | return totalDistance;
21 | }
22 |
23 | public static int calculateFallTime(double initialVelocity, double distance, double acceleration) {
24 | double velocity = Math.abs(initialVelocity);
25 | int ticks = 0;
26 |
27 | while (distance > 0) {
28 | if (ticks > MAX_TICKS)
29 | return -1;
30 |
31 | velocity += acceleration;
32 | velocity = Math.min(velocity, TERMINAL_VELOCITY);
33 | velocity *= MULTIPLIER;
34 | distance -= velocity;
35 | ticks++;
36 | }
37 |
38 | return ticks;
39 | }
40 |
41 | public static int calculateTimeToMaxVelocity(double targetVerticalVelocity, double acceleration) {
42 | double a = -acceleration * MULTIPLIER;
43 | double b = acceleration + TERMINAL_VELOCITY * MULTIPLIER;
44 | double c = -2 * targetVerticalVelocity;
45 |
46 | double discriminant = b * b - 4 * a * c;
47 | if (discriminant < 0)
48 | return 0;
49 |
50 | double positiveRoot = (-b + Math.sqrt(discriminant)) / (2 * a);
51 | return (int) Math.ceil(positiveRoot * 20);
52 | }
53 |
54 | public static double clamp(double num, double min, double max) {
55 | if (num < min)
56 | return min;
57 |
58 | return Math.min(num, max);
59 | }
60 | }
--------------------------------------------------------------------------------
/src/main/resources/config.yml:
--------------------------------------------------------------------------------
1 | #########################################
2 | # KnockbackSync Configuration #
3 | #########################################
4 |
5 | # Plugin enabled state
6 | # Toggleable using /knockbacksync toggle
7 | # Required permission: knockbacksync.toggle
8 | enabled: true
9 |
10 | # Notify staff about update availability
11 | # Required permission: knockbacksync.update
12 | notify_updates: true
13 |
14 | # Grabs the ping of combat-tagged players every x ticks
15 | # Disabling this may lead to inaccuracies in calculations
16 | runnable:
17 | enabled: true # Runnable enabled state
18 | interval: 5 # The interval in ticks between sending out pings to players
19 | combat_timer: 30 # The timer in ticks before being considered out of combat
20 |
21 | # The minimum change in ping required for it to be considered a lag spike.
22 | # If the difference between the latest and previous ping is greater than or equal to
23 | # the threshold, the previous ping value is used to avoid calculation inaccuracies.
24 | spike_threshold: 20
25 |
26 | enable_message: "&aSuccessfully enabled KnockbackSync."
27 | disable_message: "&cSuccessfully disabled KnockbackSync."
28 | player_enable_message: "&aSuccessfully enabled KnockbackSync for %player%."
29 | player_disable_message: "&cSuccessfully disabled KnockbackSync for %player%."
30 | player_ineligible_message: "&c%player% is ineligible for KnockbackSync. If you believe this is an error, please open an issue on the github page."
31 | reload_message: "&aSuccessfully reloaded KnockbackSync config."
32 |
33 | # Read FAQ!
34 | ping_offset: 25
--------------------------------------------------------------------------------
/src/main/resources/plugin.yml:
--------------------------------------------------------------------------------
1 | name: KnockbackSync
2 | version: '1.3.2'
3 | main: me.caseload.knockbacksync.KnockbackSync
4 | api-version: '1.18'
5 | authors: [ Caseload ]
6 | description: Synchronizes player knockback for smoother gameplay.
7 | folia-supported: true
8 | permissions:
9 | knockbacksync.update:
10 | default: op
11 | knockbacksync.toggle.global:
12 | default: op
13 | knockbacksync.toggle.other:
14 | default: op
15 | knockbacksync.toggle.self:
16 | default: op
17 | knockbacksync.ping:
18 | default: op
--------------------------------------------------------------------------------