59 Commits
Author SHA1 Message Date
BuildTools d54d2a199e Refractor 2024-10-11 01:56:24 -05:00
caseload b6cf670fe0 Merge pull request #20 from Axionize/bstats
Add Bstats
2024-10-09 14:46:37 -05:00
caseload decbe90910 Merge pull request #19 from Axionize/status-command
Add new /kbsync status command
2024-10-09 14:41:42 -05:00
caseload 6891b96895 Merge pull request #17 from Axionize/main
Add toggle.self and toggle.other permissions.
2024-10-09 14:40:37 -05:00
Axionize c14926037a Clarify needed permissions 2024-10-09 01:46:40 -04:00
Axionize 77198d5ed8 bStats 2024-10-09 01:04:25 -04:00
Axionize d7a262741d Disable packetevents update checks and debug mode 2024-10-08 22:58:03 -04:00
Axionize a8143785d0 Relocate all bundled deps 2024-10-08 22:49:25 -04:00
Axionize 22e5684f49 Add bStats 2024-10-08 22:47:01 -04:00
Axionize 469c2cd901 Clarify that global being off disables for everyone 2024-10-08 21:01:05 -04:00
Axionize b4f6c45704 New status command 2024-10-08 20:56:26 -04:00
Axionize 23c3e65cfb Add new toggle.self, other, and global permisssions 2024-10-08 20:50:08 -04:00
Axionize 985689c52a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/main/java/me/caseload/knockbacksync/command/subcommand/ToggleSubcommand.java
2024-10-08 20:43:23 -04:00
Axionize 0176376776 Add toggle.self and toggle.other permissions. Inform users when per-player toggling if global toggle = off 2024-10-08 13:10:29 -04:00
caseload a67fdd2426 Merge pull request #15 from Axionize/main
Add .gitignore
2024-10-08 02:18:04 -05:00
Axionize 7c1155c02c Add .gitignore 2024-10-08 03:16:25 -04:00
caseload 8e5998c2c9 Multiple important changes
Fix possible server crashes due to thread blocking
Fix NPCs being treated as real players
Reduce memory usage and increase performance
2024-10-08 01:58:55 -05:00
caseload 6543c43d7d Merge pull request #13 from Axionize/main
Prevent rare race condition from causing NPE
2024-10-07 21:07:18 -05:00
Axionize b123eb4800 Prevent rare race condition from causing NPE 2024-10-07 22:03:18 -04:00
caseload f1d74ba6ec Remove test command one more time... 2024-10-06 19:07:37 -05:00
caseload 977b9e1925 Remove test command again... 2024-10-06 19:06:55 -05:00
caseload b20eabaa0e Fix per player toggle message 2024-10-06 19:05:51 -05:00
caseload 2ebadb5d47 Delete test command 2024-10-06 17:43:08 -05:00
caseload a24466898f Delete test command 2024-10-06 17:42:51 -05:00
caseload a43d7d1543 Add per player toggle feature 2024-10-06 17:40:26 -05:00
caseload babc5fbd84 Remove debug 2024-10-06 16:06:13 -05:00
caseload bebee4f1bd Add damage ticks check 2024-10-06 05:13:32 -05:00
caseload 879c2a566a Update AttributeChangeListener.java 2024-10-06 03:53:36 -05:00
caseload 2ffa941550 Delete src/main/java/me/caseload/knockbacksync/config directory 2024-10-06 03:52:59 -05:00
caseload 6f9afb4d77 Remove unused method 2024-10-06 03:50:57 -05:00
caseload 14064d378c Delete CombatManager.java 2024-10-06 03:49:42 -05:00
caseload 3407b2165d Remove unused method 2024-10-06 03:49:22 -05:00
caseload 23d5305d42 Remove last adjustment check 2024-10-06 03:18:46 -05:00
caseload 6eaf09eeb6 remove debug 2024-10-06 03:14:21 -05:00
caseload 8b565a42e7 Add config manager 2024-10-06 03:04:31 -05:00
caseload 38723ce66b Merge pull request #11 from Axionize/exempt-support
Geyser/Floodgate support; add knockbacksync.exempt permission
2024-10-05 20:11:44 -05:00
Axionize d898d5cfeb Geyser/Floodgate support; add knockbacksync.exempt permission 2024-10-05 19:38:30 -04:00
caseload 7acc3551ee Merge pull request #10 from Axionize/gravity-optimization
Gravity optimization
2024-10-05 17:51:21 -05:00
Axionize 913303a6dc Fix gravity clamping 2024-10-05 18:18:27 -04:00
Axionize 16b122450f Optimize gravity calculation 2024-10-05 18:17:17 -04:00
caseload d0c796ef60 Merge pull request #9 from Axionize/gravity-attribute
Account for gravity attribute in 1.20.5 and newer
2024-10-05 12:01:59 -05:00
caseload fd56ae1e36 Merge pull request #8 from Axionize/reload-command
Add reload command
2024-10-05 11:56:11 -05:00
Axionize e9b2c40fac Account for gravity attribute in 1.20.5 and newer 2024-10-05 03:10:08 -04:00
Axionize f8a20b16e9 Add reload command 2024-10-05 00:54:58 -04:00
caseload 02ce284fb2 Update README.md 2024-10-04 16:48:40 -05:00
caseload 81fa80c736 Update README.md 2024-10-04 16:33:55 -05:00
caseload 81003411a0 Update README.md 2024-10-04 16:33:47 -05:00
caseload 109137a5cd Update README.md 2024-10-04 12:39:51 -05:00
caseload ba062e6f02 Update README.md 2024-10-03 22:37:53 -05:00
caseload e635d42c9a update 1.3.2 2024-10-03 21:57:54 -05:00
caseload 7f42f824ed Add last adjustment check 2024-09-30 12:53:08 -05:00
caseload 97f8b47946 fix npe and cme 2024-09-30 12:51:24 -05:00
caseload 5966e7ca73 Update README.md 2024-09-27 17:34:09 -05:00
caseload 757a35e064 Update README.md 2024-09-27 17:32:44 -05:00
caseload cd779a9d91 Update README.md 2024-09-27 16:55:00 -05:00
caseload b21c31dd5e Update README.md 2024-09-24 16:10:15 -05:00
caseload ebfc3f4b22 Update README.md 2024-09-24 15:44:40 -05:00
caseload 70a37506f7 Update README.md 2024-09-24 15:44:03 -05:00
caseload 906498456d Create README.md 2024-09-24 15:32:08 -05:00
25 changed files with 723 additions and 92 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# Ignores all of the Intellij Project settings
.idea/
# Ignore maven reduced POM built when importing/packaging project
dependency-reduced-pom.xml
# Ignore maven build files
target/
+29
View File
@@ -0,0 +1,29 @@
Minecraft doesn’t factor in network latency when determining a player's actions on the server.
This causes the server to receive outdated information that doesn’t reflect the player's clientside position.
As a result, players take negative velocity when they're on the ground clientside, but not serverside.
This plugin handles knockback as if it were calculated clientside, ensuring that no player is at a disadvantage,
regardless of their own or their opponent’s connection.
Showcase: https://www.youtube.com/watch?v=SVokpr3v-TA
Official Discord: https://discord.gg/nnpqpAtyVW
## Frequently Asked Questions (FAQ)
### Does this change put high ping players at a disadvantage?
**It depends on the player.** Some may notice a difference if they're used to relying on high ping to reduce knockback. For others, it could actually be an advantage.
### How does this change benefit high ping players?
**Knockback control.** For example, it will be easier to escape crit chains and punish crit.
### Why was the configurability of ping offset removed?
**It promotes consistency across all servers.** Extensive testing with top players has shown that an offset of 25 provides a balanced experience for everyone.
### How do I change the ping offset?
**You must run a modified build of KnockbackSync.** The variable can be changed inside of the [PlayerData](src/main/java/me/caseload/knockbacksync/manager/PlayerData.java) class.
## License
GNU General Public License v3.0 or later
See [COPYING](COPYING) to see the full text.
+32 -1
View File
@@ -6,7 +6,7 @@
<groupId>me.caseload</groupId>
<artifactId>KnockbackSync</artifactId>
<version>1.3</version>
<version>1.3.2</version>
<packaging>jar</packaging>
<name>KnockbackSync</name>
@@ -47,6 +47,22 @@
<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>
@@ -68,6 +84,10 @@
<id>codemc-releases</id>
<url>https://repo.codemc.io/repository/maven-releases/</url>
</repository>
<repository>
<id>opencollab</id>
<url>https://repo.opencollab.dev/maven-snapshots/</url>
</repository>
</repositories>
<dependencies>
@@ -98,5 +118,16 @@
<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>
@@ -5,27 +5,28 @@ import dev.jorel.commandapi.CommandAPI;
import dev.jorel.commandapi.CommandAPIBukkitConfig;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import lombok.Getter;
import lombok.Setter;
import me.caseload.knockbacksync.command.MainCommand;
import me.caseload.knockbacksync.listener.*;
import me.caseload.knockbacksync.runnable.PingRunnable;
import me.caseload.knockbacksync.manager.ConfigManager;
import me.caseload.knockbacksync.stats.BuildTypePie;
import me.caseload.knockbacksync.stats.PlayerVersionsPie;
import me.caseload.knockbacksync.stats.StatsManager;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.event.Listener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.kohsuke.github.GitHub;
@Setter
@Getter
import java.util.logging.Logger;
public final class KnockbackSync extends JavaPlugin {
private boolean toggled;
private boolean runnable;
private boolean updateAvailable;
public static Logger LOGGER;
public static JavaPlugin INSTANCE;
private long runnableInterval;
private long combatTimer;
private long spikeThreshold;
@Getter
private final ConfigManager configManager = new ConfigManager();
@Override
public void onLoad() {
@@ -36,8 +37,12 @@ public final class KnockbackSync extends JavaPlugin {
@Override
public void onEnable() {
LOGGER = getLogger();
INSTANCE = this;
checkForUpdates();
saveDefaultConfig();
configManager.loadConfig(false);
CommandAPI.onEnable();
new MainCommand().register();
@@ -48,20 +53,19 @@ public final class KnockbackSync extends JavaPlugin {
new PlayerJoinQuitListener()
);
PacketEvents.getAPI().getEventManager().registerListener(
PacketEvents.getAPI().getEventManager().registerListeners(
new AttributeChangeListener(),
new PingReceiveListener()
);
PacketEvents.getAPI().getSettings()
.checkForUpdates(false)
.debug(false);
PacketEvents.getAPI().load();
PacketEvents.getAPI().init();
toggled = getConfig().getBoolean("enabled", true);
runnable = getConfig().getBoolean("runnable.enabled", true);
runnableInterval = getConfig().getLong("runnable.interval", 5L);
combatTimer = getConfig().getLong("runnable.timer", 30L);
spikeThreshold = getConfig().getLong("spike_threshold", 25L);
if (runnable)
new PingRunnable().runTaskTimerAsynchronously(this, 0L, runnableInterval);
StatsManager.init();
}
@Override
@@ -94,14 +98,14 @@ public final class KnockbackSync extends JavaPlugin {
boolean updateAvailable = !currentVersion.equalsIgnoreCase(latestVersion);
if (updateAvailable) {
getLogger().warning("A new update is available for download at: https://github.com/CASELOAD7000/knockback-sync/releases/latest");
LOGGER.warning("A new update is available for download at: https://github.com/CASELOAD7000/knockback-sync/releases/latest");
} else {
getLogger().info("You are running the latest release.");
LOGGER.info("You are running the latest release.");
}
setUpdateAvailable(updateAvailable);
configManager.setUpdateAvailable(updateAvailable);
} catch (Exception e) {
getLogger().severe("Failed to check for updates: " + e.getMessage());
LOGGER.severe("Failed to check for updates: " + e.getMessage());
}
});
}
@@ -2,6 +2,8 @@ package me.caseload.knockbacksync.command;
import dev.jorel.commandapi.CommandAPICommand;
import me.caseload.knockbacksync.command.subcommand.PingSubcommand;
import me.caseload.knockbacksync.command.subcommand.ReloadSubcommand;
import me.caseload.knockbacksync.command.subcommand.StatusSubcommand;
import me.caseload.knockbacksync.command.subcommand.ToggleSubcommand;
import org.bukkit.ChatColor;
@@ -12,6 +14,8 @@ public class MainCommand {
.withAliases("kbsync")
.withSubcommand(new PingSubcommand().getCommand())
.withSubcommand(new ToggleSubcommand().getCommand())
.withSubcommand(new ReloadSubcommand().getCommand())
.withSubcommand(new StatusSubcommand().getCommand())
.executes((sender, args) -> {
sender.sendMessage(ChatColor.translateAlternateColorCodes(
'&',
@@ -0,0 +1,25 @@
package me.caseload.knockbacksync.command.subcommand;
import dev.jorel.commandapi.CommandAPICommand;
import me.caseload.knockbacksync.KnockbackSync;
import me.caseload.knockbacksync.manager.ConfigManager;
import org.bukkit.ChatColor;
import org.bukkit.event.Listener;
public class ReloadSubcommand implements Listener {
public CommandAPICommand getCommand() {
return new CommandAPICommand("reload")
.withPermission("knockbacksync.reload")
.executes((sender, args) -> {
ConfigManager configManager = KnockbackSync.getInstance().getConfigManager();
configManager.loadConfig(true);
String reloadMessage = ChatColor.translateAlternateColorCodes('&',
configManager.getReloadMessage()
);
sender.sendMessage(reloadMessage);
});
}
}
@@ -0,0 +1,58 @@
package me.caseload.knockbacksync.command.subcommand;
import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.CommandPermission;
import dev.jorel.commandapi.arguments.PlayerArgument;
import me.caseload.knockbacksync.KnockbackSync;
import me.caseload.knockbacksync.manager.ConfigManager;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import java.util.UUID;
public class StatusSubcommand implements Listener {
public CommandAPICommand getCommand() {
return new CommandAPICommand("status")
.withPermission(CommandPermission.NONE)
.withOptionalArguments(new PlayerArgument("target"))
.executes((sender, args) -> {
ConfigManager configManager = KnockbackSync.getInstance().getConfigManager();
Player target = (Player) args.get("target");
// Show global status
boolean globalStatus = configManager.isToggled();
sender.sendMessage(ChatColor.YELLOW + "Global KnockbackSync status: " +
(globalStatus ? ChatColor.GREEN + "Enabled" : ChatColor.RED + "Disabled"));
// Show player status
if (target == null && sender instanceof Player)
target = (Player) sender;
if (target == null)
return;
if (sender.hasPermission("knockbacksync.status.self") ||
(sender.hasPermission("knockbacksync.status.other") && !sender.equals(target))) {
UUID uuid = target.getUniqueId();
boolean playerStatus = !PlayerDataManager.containsPlayerData(uuid);
if (!globalStatus) {
sender.sendMessage(ChatColor.YELLOW + target.getName() + "'s KnockbackSync status: " +
ChatColor.RED + "Disabled (Global toggle is off)");
} else {
sender.sendMessage(ChatColor.YELLOW + target.getName() + "'s KnockbackSync status: " +
(playerStatus ? ChatColor.GREEN + "Enabled" : ChatColor.RED + "Disabled"));
}
} else {
sender.sendMessage(ChatColor.RED + "You don't have the "
+ (sender.equals(target) ? "knockbacksync.status.self" : "knockbacksync.status.other") +
" permission needed to check status for " +
(sender.equals(target) ? "yourself" : "other players") + ".");
}
});
}
}
@@ -1,28 +1,86 @@
package me.caseload.knockbacksync.command.subcommand;
import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.CommandPermission;
import dev.jorel.commandapi.arguments.PlayerArgument;
import me.caseload.knockbacksync.KnockbackSync;
import me.caseload.knockbacksync.manager.ConfigManager;
import me.caseload.knockbacksync.manager.PlayerData;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Listener;
public class ToggleSubcommand {
import java.util.UUID;
public class ToggleSubcommand implements Listener {
public CommandAPICommand getCommand() {
return new CommandAPICommand("toggle")
.withPermission("knockbacksync.toggle")
.withPermission(CommandPermission.NONE)
.withOptionalArguments(new PlayerArgument("target"))
.executes((sender, args) -> {
boolean toggledState = !KnockbackSync.getInstance().isToggled();
ConfigManager configManager = KnockbackSync.getInstance().getConfigManager();
Player target = (Player) args.get("target");
String message;
KnockbackSync.getInstance().getConfig().set("enabled", toggledState);
KnockbackSync.getInstance().saveConfig();
KnockbackSync.getInstance().setToggled(toggledState);
if (target == null) {
// Global toggle
if (sender.hasPermission("knockbacksync.toggle.global")) {
boolean toggledState = !configManager.isToggled();
configManager.setToggled(toggledState);
String message = ChatColor.translateAlternateColorCodes('&',
toggledState ?
KnockbackSync.getInstance().getConfig().getString("enable_message", "&aSuccessfully enabled KnockbackSync.") :
KnockbackSync.getInstance().getConfig().getString("disable_message", "&cSuccessfully disabled KnockbackSync.")
);
KnockbackSync.getInstance().getConfig().set("enabled", toggledState);
KnockbackSync.getInstance().saveConfig();
sender.sendMessage(message);
message = ChatColor.translateAlternateColorCodes('&',
toggledState ? configManager.getEnableMessage() : configManager.getDisableMessage()
);
} else {
message = ChatColor.RED + "You don't have permission to toggle the global setting.";
}
sender.sendMessage(message);
} else {
// Player-specific toggle
if (!configManager.isToggled()) {
message = ChatColor.RED + "Knockbacksync is currently disabled on this server. Contact your server administrator for more information.";
sender.sendMessage(message);
} else if (sender instanceof Player && sender.equals(target) && sender.hasPermission("knockbacksync.toggle.self")) {
togglePlayerKnockback(target, configManager, sender);
} else if (sender.hasPermission("knockbacksync.toggle.other")) {
togglePlayerKnockback(target, configManager, sender);
} else {
message = ChatColor.RED + "You don't have permission to toggle knockback for " +
(sender.equals(target) ? "yourself" : "other players") + ".";
sender.sendMessage(message);
}
}
});
}
private void togglePlayerKnockback(Player target, ConfigManager configManager, CommandSender sender) {
UUID uuid = target.getUniqueId();
if (PlayerDataManager.shouldExempt(uuid)) {
String message = ChatColor.translateAlternateColorCodes('&',
configManager.getPlayerIneligibleMessage()
).replace("%player%", target.getName());
sender.sendMessage(message);
return;
}
boolean hasPlayerData = PlayerDataManager.containsPlayerData(uuid);
if (hasPlayerData)
PlayerDataManager.removePlayerData(uuid);
else
PlayerDataManager.addPlayerData(uuid, new PlayerData(target));
String message = ChatColor.translateAlternateColorCodes('&',
hasPlayerData ? configManager.getPlayerDisableMessage() : configManager.getPlayerEnableMessage()
).replace("%player%", target.getName());
sender.sendMessage(message);
}
}
@@ -0,0 +1,97 @@
package me.caseload.knockbacksync.listener;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateAttributes;
import me.caseload.knockbacksync.manager.PlayerData;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import me.caseload.knockbacksync.util.MathUtil;
import org.bukkit.entity.Player;
import java.util.List;
import java.util.UUID;
public class AttributeChangeListener extends PacketListenerAbstract {
public static final UUID SPRINTING_MODIFIER_UUID =
UUID.fromString("662A6B8D-DA3E-4C1C-8813-96EA6097278D");
double minGravity = -1;
double maxGravity = 1;
double defaultGravity = 0.08;
double currentGravity;
public AttributeChangeListener() {
currentGravity = defaultGravity;
}
@Override
public void onPacketSend(PacketSendEvent event) {
// Intercept the packet that updates entity properties (attributes)
if (event.getPacketType() != PacketType.Play.Server.UPDATE_ATTRIBUTES)
return;
WrapperPlayServerUpdateAttributes packet = new WrapperPlayServerUpdateAttributes(event);
// Check if the entity is a player
Player player = event.getPlayer();
if (player == null || !PlayerDataManager.containsPlayerData(player.getUniqueId()))
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(player, calculateValueWithModifiers(property));
break;
}
}
}
public double calculateValueWithModifiers(WrapperPlayServerUpdateAttributes.Property property) {
double baseValue = property.getValue();
double additionSum = 0;
double multiplyBaseSum = 0;
double multiplyTotalProduct = 1.0;
List<WrapperPlayServerUpdateAttributes.PropertyModifier> modifiers = property.getModifiers();
// TODO, account for https://bugs.mojang.com/browse/MC-69459 ?
// modifiers.removeIf(modifier -> modifier.getUUID().equals(SPRINTING_MODIFIER_UUID) || modifier.getName().getKey().equals("sprinting"));
for (WrapperPlayServerUpdateAttributes.PropertyModifier modifier : modifiers) {
switch (modifier.getOperation()) {
case ADDITION:
additionSum += modifier.getAmount();
break;
case MULTIPLY_BASE:
multiplyBaseSum += modifier.getAmount();
break;
case MULTIPLY_TOTAL:
multiplyTotalProduct *= (1.0 + modifier.getAmount());
break;
}
}
double newValue = (baseValue + additionSum) * (1 + multiplyBaseSum) * multiplyTotalProduct;
newValue = MathUtil.clamp(newValue, minGravity, maxGravity);
if (newValue < minGravity || newValue > maxGravity)
throw new IllegalArgumentException("New value must be between min and max!");
return this.currentGravity = newValue;
}
// Yes this is not properly latency compensated, that would require including a proper simulation engine
// Laggy players will just have to deal with being on the wrong gravity for a few hundred ms, too bad!
public void onPlayerGravityChange(Player player, double newGravity) {
PlayerData playerData = PlayerDataManager.getPlayerData(player.getUniqueId());
if (playerData.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_5)) {
playerData.setGravity(newGravity);
} else {
currentGravity = defaultGravity;
}
}
}
@@ -13,7 +13,7 @@ public class PingReceiveListener extends PacketListenerAbstract {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (!KnockbackSync.getInstance().isToggled())
if (!KnockbackSync.getInstance().getConfigManager().isToggled())
return;
if (event.getPacketType() != PacketType.Play.Client.PONG)
@@ -22,6 +22,12 @@ public class PingReceiveListener extends PacketListenerAbstract {
Player player = event.getPlayer();
PlayerData playerData = PlayerDataManager.getPlayerData(player.getUniqueId());
// If player is timed out by the server and removed from the map on the main thread
// The server can still receive ping packets from the disconnected client
// At which point the entry will no longer be in the map but this code will be processed!
if (playerData == null)
return;
int packetId = new WrapperPlayClientPong(event).getId();
Long sendTime = playerData.getTimeline().get(packetId);
@@ -12,17 +12,21 @@ public class PlayerDamageListener implements Listener {
@EventHandler(ignoreCancelled = true)
public void onPlayerDamage(EntityDamageByEntityEvent event) {
if (!KnockbackSync.getInstance().isToggled())
if (!KnockbackSync.getInstance().getConfigManager().isToggled())
return;
if (!(event.getEntity() instanceof Player victim) || !(event.getDamager() instanceof Player attacker))
return;
PlayerData playerData = PlayerDataManager.getPlayerData(victim.getUniqueId());
if (playerData == null)
return;
playerData.setVerticalVelocity(playerData.calculateVerticalVelocity(attacker)); // do not move this calculation
playerData.setLastDamageTicks(victim.getNoDamageTicks());
playerData.updateCombat();
if (!KnockbackSync.getInstance().isRunnable())
if (!KnockbackSync.getInstance().getConfigManager().isRunnableEnabled())
playerData.sendPing();
}
}
@@ -8,7 +8,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.UUID;
@@ -20,7 +19,7 @@ public class PlayerJoinQuitListener implements Listener {
Player player = event.getPlayer();
PlayerDataManager.addPlayerData(player.getUniqueId(), new PlayerData(player));
if (KnockbackSync.getInstance().isUpdateAvailable() && player.hasPermission("knockbacksync.update"))
if (KnockbackSync.getInstance().getConfigManager().isUpdateAvailable() && KnockbackSync.getInstance().getConfigManager().isNotifyUpdate() && player.hasPermission("knockbacksync.update"))
player.sendMessage(ChatColor.translateAlternateColorCodes(
'&',
"&6An updated version of &eKnockbackSync &6is now available for download at: &bhttps://github.com/CASELOAD7000/knockback-sync/releases/latest"
@@ -31,17 +30,8 @@ public class PlayerJoinQuitListener implements Listener {
public void onPlayerQuit(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
if (playerData.isInCombat())
playerData.quitCombat(true);
PlayerDataManager.removePlayerData(uuid);
}
@EventHandler
public void onPlayerKick(PlayerKickEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
PlayerData playerData = PlayerDataManager.getPlayerData(uuid);
if (playerData == null)
return;
if (playerData.isInCombat())
playerData.quitCombat(true);
@@ -17,12 +17,10 @@ public class PlayerKnockbackListener implements Listener {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerVelocity(PlayerVelocityEvent event) {
if (!KnockbackSync.getInstance().isToggled())
if (!KnockbackSync.getInstance().getConfigManager().isToggled())
return;
Player victim = event.getPlayer();
PlayerData playerData = PlayerDataManager.getPlayerData(victim.getUniqueId());
EntityDamageEvent entityDamageEvent = victim.getLastDamageCause();
if (entityDamageEvent == null)
return;
@@ -35,9 +33,16 @@ public class PlayerKnockbackListener implements Listener {
if (!(attacker instanceof Player))
return;
PlayerData playerData = PlayerDataManager.getPlayerData(victim.getUniqueId());
if (playerData == null)
return;
Integer damageTicks = playerData.getLastDamageTicks();
if (damageTicks != null && damageTicks > 8)
return;
Vector velocity = victim.getVelocity();
Double verticalVelocity = playerData.getVerticalVelocity();
if (verticalVelocity == null || !playerData.isOnGround(velocity.getY()))
return;
@@ -2,17 +2,16 @@ package me.caseload.knockbacksync.manager;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class CombatManager {
private static final Set<UUID> combatPlayers = new HashSet<>();
private static final Set<UUID> combatPlayers = ConcurrentHashMap.newKeySet();
public static @NotNull Set<UUID> getPlayers() {
return Collections.unmodifiableSet(combatPlayers);
return combatPlayers;
}
public static void addPlayer(UUID uuid) {
@@ -0,0 +1,61 @@
package me.caseload.knockbacksync.manager;
import lombok.Getter;
import lombok.Setter;
import me.caseload.knockbacksync.KnockbackSync;
import me.caseload.knockbacksync.runnable.PingRunnable;
import org.bukkit.scheduler.BukkitTask;
@Getter
@Setter
public class ConfigManager {
private boolean toggled;
private boolean runnableEnabled;
private boolean updateAvailable;
private boolean notifyUpdate;
private long runnableInterval;
private long combatTimer;
private long spikeThreshold;
private String enableMessage;
private String disableMessage;
private String playerEnableMessage;
private String playerDisableMessage;
private String playerIneligibleMessage;
private String reloadMessage;
private BukkitTask pingTask;
public void loadConfig(boolean reloadConfig) {
KnockbackSync instance = KnockbackSync.getInstance();
if (reloadConfig)
instance.reloadConfig();
toggled = instance.getConfig().getBoolean("enabled", true);
// Checks to see if the runnable was enabled...
// and if we now want to disable it
boolean newRunnableEnabled = instance.getConfig().getBoolean("runnable.enabled", true);
if (runnableEnabled && newRunnableEnabled && pingTask != null) // null check for first startup
pingTask.cancel();
runnableEnabled = newRunnableEnabled;
if (runnableEnabled)
pingTask = new PingRunnable().runTaskTimerAsynchronously(instance, 0L, runnableInterval);
notifyUpdate = instance.getConfig().getBoolean("notify_updates", true);
runnableInterval = instance.getConfig().getLong("runnable.interval", 5L);
combatTimer = instance.getConfig().getLong("runnable.timer", 30L);
spikeThreshold = instance.getConfig().getLong("spike_threshold", 20L);
enableMessage = instance.getConfig().getString("enable_message", "&aSuccessfully enabled KnockbackSync.");
disableMessage = instance.getConfig().getString("disable_message", "&cSuccessfully disabled KnockbackSync.");
playerEnableMessage = instance.getConfig().getString("player_enable_message", "&aSuccessfully enabled KnockbackSync for %player%.");
playerDisableMessage = instance.getConfig().getString("player_disable_message", "&cSuccessfully disabled KnockbackSync for %player%.");
playerIneligibleMessage = instance.getConfig().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 = instance.getConfig().getString("reload_message", "&aSuccessfully reloaded KnockbackSync.");
}
}
@@ -1,6 +1,8 @@
package me.caseload.knockbacksync.manager;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPing;
import lombok.Getter;
import lombok.Setter;
@@ -8,6 +10,7 @@ import me.caseload.knockbacksync.KnockbackSync;
import me.caseload.knockbacksync.util.MathUtil;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
@@ -27,8 +30,10 @@ public class PlayerData {
private final Player player;
public final User user;
// Please read the GitHub FAQ before adjusting.
private static final long PING_OFFSET = 20;
private static final long PING_OFFSET = 25;
@NotNull
private final Map<Integer, Long> timeline = new HashMap<>();
@@ -45,8 +50,15 @@ public class PlayerData {
@Nullable @Setter
private Double verticalVelocity;
@Nullable @Setter
private Integer lastDamageTicks;
@Setter
private double gravity = 0.08;
public PlayerData(Player player) {
this.player = player;
this.user = PacketEvents.getAPI().getPlayerManager().getUser(player);
}
/**
@@ -59,7 +71,7 @@ public class PlayerData {
public long getEstimatedPing() {
long currentPing = (ping != null) ? ping : player.getPing();
long lastPing = (previousPing != null) ? previousPing : player.getPing();
long ping = (currentPing - lastPing > KnockbackSync.getInstance().getSpikeThreshold()) ? lastPing : currentPing;
long ping = (currentPing - lastPing > KnockbackSync.getInstance().getConfigManager().getSpikeThreshold()) ? lastPing : currentPing;
return Math.max(1, ping - PING_OFFSET);
}
@@ -74,14 +86,10 @@ public class PlayerData {
}
/**
* Determines if the Player is on the ground, either serverside or clientside.
* Determines if the Player is on the ground clientside, but not serverside
* <p>
* Returns <code>ping ≥ (tMax + tFall)</code> and <code>gDist ≤ 1.3</code>
* <p>
* Returns <code>true</code> if:
* <ul>
* <li>Clientside: <code>ping ≥ (tMax + tFall)</code> and <code>gDist ≤ 1.3</code></li>
* <li>Serverside: <code>gDist ≤ 0</code></li>
* </ul>
*
* Where:
* <ul>
* <li><code>ping</code>: Estimated latency</li>
@@ -104,11 +112,14 @@ public class PlayerData {
double gDist = getDistanceToGround();
if (gDist <= 0)
return true;
return false; // prevent player from taking adjusted knockback when on ground serverside
int tMax = verticalVelocity > 0 ? MathUtil.calculateTimeToMaxVelocity(verticalVelocity) : 0;
double mH = verticalVelocity > 0 ? MathUtil.calculateDistanceTraveled(verticalVelocity, tMax) : 0;
int tFall = MathUtil.calculateFallTime(verticalVelocity, mH + gDist);
int tMax = verticalVelocity > 0 ? MathUtil.calculateTimeToMaxVelocity(verticalVelocity, gravity) : 0;
double mH = verticalVelocity > 0 ? MathUtil.calculateDistanceTraveled(verticalVelocity, tMax, gravity) : 0;
int tFall = MathUtil.calculateFallTime(verticalVelocity, mH + gDist, gravity);
if (tFall == -1)
return false; // reached the max tick limit, not safe to predict
return getEstimatedPing() >= tMax + tFall / 20.0 * 1000 && gDist <= 1.3;
}
@@ -172,6 +183,10 @@ public class PlayerData {
yAxis -= resistanceFactor;
}
// vertical velocity is always 0.4 when you have knockback level higher than 0
if (attacker.getInventory().getItemInMainHand().getEnchantmentLevel(Enchantment.KNOCKBACK) > 0)
yAxis = 0.4;
return yAxis;
}
@@ -212,6 +227,15 @@ public class PlayerData {
@NotNull
private BukkitTask newCombatTask() {
return Bukkit.getScheduler().runTaskLaterAsynchronously(KnockbackSync.getInstance(),
() -> quitCombat(false), KnockbackSync.getInstance().getCombatTimer());
() -> quitCombat(false), KnockbackSync.getInstance().getConfigManager().getCombatTimer());
}
public ClientVersion getClientVersion() {
ClientVersion ver = user.getClientVersion();
if (ver == null) {
// If temporarily null, assume server version...
return ClientVersion.getById(PacketEvents.getAPI().getServerManager().getVersion().getProtocolVersion());
}
return ver;
}
}
@@ -1,22 +1,42 @@
package me.caseload.knockbacksync.manager;
import io.github.retrooper.packetevents.util.GeyserUtil;
import me.caseload.knockbacksync.util.FloodgateUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class PlayerDataManager {
private static final Map<UUID, PlayerData> playerDataMap = new HashMap<>();
private static final Map<UUID, PlayerData> playerDataMap = new ConcurrentHashMap<>();
public static @NotNull PlayerData getPlayerData(UUID uuid) {
public static PlayerData getPlayerData(@NotNull UUID uuid) {
return playerDataMap.get(uuid);
}
public static void addPlayerData(UUID uuid, PlayerData playerData) {
playerDataMap.put(uuid, playerData);
public static void addPlayerData(@NotNull UUID uuid, @NotNull PlayerData playerData) {
if (!shouldExempt(uuid))
playerDataMap.put(uuid, playerData);
}
public static void removePlayerData(UUID uuid) {
public static void removePlayerData(@NotNull UUID uuid) {
playerDataMap.remove(uuid);
}
public static boolean containsPlayerData(@NotNull UUID uuid) {
return playerDataMap.containsKey(uuid);
}
public static boolean shouldExempt(@NotNull UUID uuid) {
// Geyser players don't have Java movement
return GeyserUtil.isGeyserPlayer(uuid)
// Floodgate is the authentication system for Geyser on servers that use Geyser as a proxy instead of installing it as a plugin directly on the server
|| FloodgateUtil.isFloodgatePlayer(uuid)
// Geyser formatted player string
// This will never happen for Java players, as the first character in the 3rd group is always 4 (xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx)
|| uuid.toString().startsWith("00000000-0000-0000-0009");
}
}
@@ -12,7 +12,7 @@ public class PingRunnable extends BukkitRunnable {
@Override
public void run() {
if (!KnockbackSync.getInstance().isToggled())
if (!KnockbackSync.getInstance().getConfigManager().isToggled())
return;
for (UUID uuid : CombatManager.getPlayers()) {
@@ -0,0 +1,115 @@
package me.caseload.knockbacksync.stats;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import me.caseload.knockbacksync.KnockbackSync;
import org.bstats.charts.SimplePie;
import org.bukkit.Bukkit;
import org.kohsuke.github.GHAsset;
import org.kohsuke.github.GHRelease;
import org.kohsuke.github.GitHub;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.List;
public class BuildTypePie extends SimplePie {
private static final String RELEASES_FILE = "releases.txt";
private static final String DEV_BUILDS_FILE = "dev-builds.txt";
private static final File dataFolder = KnockbackSync.INSTANCE.getDataFolder();
private static String cachedBuildType = null;
public BuildTypePie() {
super("build_type", BuildTypePie::determineBuildType);
}
public static String determineBuildType() {
if (cachedBuildType == null) {
cachedBuildType = calculateBuildType();
}
return cachedBuildType;
}
private static String calculateBuildType() {
try {
String currentHash = getPluginJarHash();
downloadBuildFiles();
if (isHashInFile(currentHash, new File(dataFolder, RELEASES_FILE))) {
return "release";
} else if (isHashInFile(currentHash, new File(dataFolder, DEV_BUILDS_FILE))) {
return "dev";
} else {
return "fork";
}
} catch (Exception e) {
e.printStackTrace();
return "unknown";
}
}
private static void downloadBuildFiles() throws IOException {
GitHub gitHub = GitHub.connectAnonymously();
GHRelease latestRelease = gitHub.getRepository("Axionize/knockback-sync")
.getLatestRelease();
List<GHAsset> assets = latestRelease.listAssets().toList();
for (GHAsset asset : assets) {
if (asset.getName().equals(RELEASES_FILE) || asset.getName().equals(DEV_BUILDS_FILE)) {
KnockbackSync.INSTANCE.getLogger().info("Downloading: " + asset.getName());
String jsonContent = readStringFromURL(asset.getUrl().toString());
JsonObject jsonObject = JsonParser.parseString(jsonContent).getAsJsonObject();
String downloadUrl = jsonObject.get("browser_download_url").getAsString();
try (InputStream inputStream = new URL(downloadUrl).openStream();
FileOutputStream outputStream = new FileOutputStream(new File(dataFolder, asset.getName()))) {
inputStream.transferTo(outputStream);
}
KnockbackSync.INSTANCE.getLogger().info("Downloaded: " + asset.getName());
}
}
}
private static boolean isHashInFile(String hash, File file) throws IOException {
if (!file.exists()) {
return false;
}
List<String> lines = Files.readAllLines(Paths.get(file.getPath()));
return lines.contains(hash);
}
private static String getPluginJarHash() throws Exception {
URL jarUrl = Bukkit.getPluginManager().getPlugin("KnockbackSync").getClass().getProtectionDomain().getCodeSource().getLocation();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream is = jarUrl.openStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
}
byte[] hash = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
private static String readStringFromURL(String urlString) throws IOException {
try (InputStream inputStream = new URL(urlString).openStream()) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,25 @@
package me.caseload.knockbacksync.stats;
import me.caseload.knockbacksync.manager.PlayerData;
import me.caseload.knockbacksync.manager.PlayerDataManager;
import org.bstats.charts.AdvancedPie;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
public class PlayerVersionsPie extends AdvancedPie {
// Gets the client versions of players online
public PlayerVersionsPie() {
super("player_version", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (Player player : Bukkit.getOnlinePlayers()) {
PlayerData playerData = PlayerDataManager.getPlayerData(player.getUniqueId());
valueMap.put(playerData.getClientVersion().toString(), valueMap.getOrDefault(playerData.getClientVersion().toString(), 0) + 1);
}
return valueMap;
});
}
}
@@ -0,0 +1,19 @@
package me.caseload.knockbacksync.stats;
import me.caseload.knockbacksync.KnockbackSync;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
public class StatsManager {
public static Metrics metrics;
public static void init() {
Bukkit.getScheduler().runTaskAsynchronously(KnockbackSync.getInstance(), () -> {
BuildTypePie.determineBuildType(); // Function to calculate hash
metrics = new Metrics(KnockbackSync.INSTANCE, 23568);
metrics.addCustomChart(new PlayerVersionsPie());
metrics.addCustomChart(new BuildTypePie());
});
}
}
@@ -0,0 +1,31 @@
package me.caseload.knockbacksync.util;
import org.geysermc.floodgate.api.FloodgateApi;
import java.util.UUID;
public class FloodgateUtil {
private static boolean CHECKED_FOR_FLOODGATE;
private static boolean FLOODGATE_PRESENT;
public static boolean isFloodgatePlayer(UUID uuid) {
if (!CHECKED_FOR_FLOODGATE) {
try {
Class.forName("org.geysermc.floodgate.api.FloodgateApi");
FLOODGATE_PRESENT = true;
} catch (ClassNotFoundException e) {
FLOODGATE_PRESENT = false;
}
CHECKED_FOR_FLOODGATE = true;
}
if (FLOODGATE_PRESENT) {
return FloodgateApi.getInstance().isFloodgatePlayer(uuid);
} else {
return false;
}
}
}
@@ -3,29 +3,32 @@ package me.caseload.knockbacksync.util;
public class MathUtil {
private static final double TERMINAL_VELOCITY = 3.92;
private static final double GRAVITY = 0.08;
private static final double MULTIPLIER = 0.98;
private static final int MAX_TICKS = 20;
public static double calculateDistanceTraveled(double velocity, int time)
public static double calculateDistanceTraveled(double velocity, int time, double acceleration)
{
double totalDistance = 0;
for (int i = 0; i < time; i++)
{
totalDistance += velocity;
velocity = ((velocity - GRAVITY) * MULTIPLIER);
velocity = ((velocity - acceleration) * MULTIPLIER);
velocity = Math.min(velocity, TERMINAL_VELOCITY);
}
return totalDistance;
}
public static int calculateFallTime(double initialVelocity, double distance) {
public static int calculateFallTime(double initialVelocity, double distance, double acceleration) {
double velocity = Math.abs(initialVelocity);
int ticks = 0;
while (distance > 0) {
velocity += GRAVITY;
if (ticks > MAX_TICKS)
return -1;
velocity += acceleration;
velocity = Math.min(velocity, TERMINAL_VELOCITY);
velocity *= MULTIPLIER;
distance -= velocity;
@@ -35,9 +38,9 @@ public class MathUtil {
return ticks;
}
public static int calculateTimeToMaxVelocity(double targetVerticalVelocity) {
double a = -GRAVITY * MULTIPLIER;
double b = GRAVITY + TERMINAL_VELOCITY * MULTIPLIER;
public static int calculateTimeToMaxVelocity(double targetVerticalVelocity, double acceleration) {
double a = -acceleration * MULTIPLIER;
double b = acceleration + TERMINAL_VELOCITY * MULTIPLIER;
double c = -2 * targetVerticalVelocity;
double discriminant = b * b - 4 * a * c;
@@ -47,4 +50,11 @@ public class MathUtil {
double positiveRoot = (-b + Math.sqrt(discriminant)) / (2 * a);
return (int) Math.ceil(positiveRoot * 20);
}
public static double clamp(double num, double min, double max) {
if (num < min)
return min;
return Math.min(num, max);
}
}
+6 -2
View File
@@ -21,7 +21,11 @@ runnable:
# The minimum change in ping required for it to be considered a lag spike.
# If the difference between the latest and previous ping is greater than or equal to
# the threshold, the previous ping value is used to avoid calculation inaccuracies.
spike_threshold: 25
spike_threshold: 20
enable_message: "&aSuccessfully enabled KnockbackSync."
disable_message: "&cSuccessfully disabled KnockbackSync."
disable_message: "&cSuccessfully disabled KnockbackSync."
player_enable_message: "&aSuccessfully enabled KnockbackSync for %player%."
player_disable_message: "&cSuccessfully disabled KnockbackSync 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."
+6 -2
View File
@@ -1,5 +1,5 @@
name: KnockbackSync
version: '1.3'
version: '1.3.2'
main: me.caseload.knockbacksync.KnockbackSync
api-version: '1.18'
authors: [ Caseload ]
@@ -7,7 +7,11 @@ description: Synchronizes player knockback for smoother gameplay.
permissions:
knockbacksync.update:
default: op
knockbacksync.toggle:
knockbacksync.toggle.global:
default: op
knockbacksync.toggle.other:
default: op
knockbacksync.toggle.self:
default: op
knockbacksync.ping:
default: op