commit 4aa069ca7937c7b68c1fcf4969b5618932802d32 Author: YuruzukiOvO Date: Fri Nov 21 22:32:04 2025 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..162ad0d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/libs/ProtocolLib.jar b/libs/ProtocolLib.jar new file mode 100644 index 0000000..e86e173 Binary files /dev/null and b/libs/ProtocolLib.jar differ diff --git a/libs/spigot-1.8.8-R0.1-SNAPSHOT-latest.jar b/libs/spigot-1.8.8-R0.1-SNAPSHOT-latest.jar new file mode 100644 index 0000000..300d172 Binary files /dev/null and b/libs/spigot-1.8.8-R0.1-SNAPSHOT-latest.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8023215 --- /dev/null +++ b/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + me.dw1e.kbm + KnockBackManager + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + org.github.spigot + 1.8.8 + 1.8.8 + system + ${basedir}/libs/spigot-1.8.8-R0.1-SNAPSHOT-latest.jar + + + com.comphenix.protocol + ProtocolLib + 5.1.0 + system + ${basedir}/libs/ProtocolLib.jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 8 + 8 + + + + + \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/KnockbackManager.java b/src/main/java/me/dw1e/kbm/KnockbackManager.java new file mode 100644 index 0000000..6535cbc --- /dev/null +++ b/src/main/java/me/dw1e/kbm/KnockbackManager.java @@ -0,0 +1,101 @@ +package me.dw1e.kbm; + +import me.dw1e.kbm.api.KnockbackManagerAPI; +import me.dw1e.kbm.commands.KBMCommandHandler; +import me.dw1e.kbm.config.ConfigManager; +import me.dw1e.kbm.data.PlayerDataManager; +import me.dw1e.kbm.listeners.PacketListener; +import me.dw1e.kbm.listeners.PlayerEventListener; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.plugin.java.JavaPlugin; + +public final class KnockbackManager extends JavaPlugin { + private static KnockbackManager instance; + private ConfigManager configManager; + private PlayerDataManager playerDataManager; + private KnockbackManagerAPI api; + private PacketListener packetListener; + private int serverTick; + + private final String pluginPrefix = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "KBM" + ChatColor.DARK_GRAY + "] "; + + @Override + public void onEnable() { + instance = this; + + if (!getDescription().getName().equals("KnockbackManager") || + !getDescription().getAuthors().contains("dw1e")) { + getLogger().severe("检测到插件信息篡改,已停止运行!"); + Bukkit.getPluginManager().disablePlugin(this); + return; + } + + this.configManager = new ConfigManager(this); + this.playerDataManager = new PlayerDataManager(); + this.api = new KnockbackManagerAPIImpl(this); + + this.configManager.loadConfigs(Bukkit.getConsoleSender()); + + this.playerDataManager.registerAllPlayers(); + + getCommand("kbm").setExecutor(new KBMCommandHandler(this)); + + Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this), this); + + Bukkit.getScheduler().runTaskTimer(this, () -> serverTick++, 0L, 1L); + + setupProtocolLib(); + + getLogger().info("Knockback Manager 已启用!"); + } + + @Override + public void onDisable() { + if (packetListener != null) { + packetListener.unregister(); + } + + if (playerDataManager != null) { + playerDataManager.clearAllData(); + } + + instance = null; + getLogger().info("Knockback Manager 已禁用!"); + } + + private void setupProtocolLib() { + org.bukkit.plugin.Plugin protocolLib = Bukkit.getPluginManager().getPlugin("ProtocolLib"); + if (protocolLib != null && protocolLib.isEnabled()) { + String version = protocolLib.getDescription().getVersion(); + try { + int minorVersion = Integer.parseInt(version.split("\\.")[1]); + if (minorVersion < 5) { + logMessage(ChatColor.RED + "不支持的 ProtocolLib 版本: " + ChatColor.YELLOW + version + + ChatColor.RED + ", 请使用 5.0.0 或之后的版本, Misplace 模块将不会启用!"); + } else { + logMessage(ChatColor.GREEN + "检测到 ProtocolLib " + ChatColor.YELLOW + version + + ChatColor.GREEN + ", 已启用 Misplace 模块!"); + this.packetListener = new PacketListener(this); + this.packetListener.register(); + } + } catch (Exception e) { + logMessage(ChatColor.RED + "解析 ProtocolLib 版本时出错, Misplace 模块将不会启用!"); + } + } else { + logMessage(ChatColor.RED + "未检测到 ProtocolLib 安装/运行, Misplace 模块将不会启用!"); + } + } + + public void logMessage(String message) { + Bukkit.getConsoleSender().sendMessage(pluginPrefix + message); + } + + // Getters + public static KnockbackManager getInstance() { return instance; } + public ConfigManager getConfigManager() { return configManager; } + public PlayerDataManager getPlayerDataManager() { return playerDataManager; } + public KnockbackManagerAPI getAPI() { return api; } + public int getServerTick() { return serverTick; } + public String getPluginPrefix() { return pluginPrefix; } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/KnockbackManagerAPIImpl.java b/src/main/java/me/dw1e/kbm/KnockbackManagerAPIImpl.java new file mode 100644 index 0000000..36adf1e --- /dev/null +++ b/src/main/java/me/dw1e/kbm/KnockbackManagerAPIImpl.java @@ -0,0 +1,68 @@ +package me.dw1e.kbm; + +import me.dw1e.kbm.api.KnockbackManagerAPI; +import me.dw1e.kbm.config.ConfigHolder; +import me.dw1e.kbm.data.PlayerKnockbackData; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +public class KnockbackManagerAPIImpl implements KnockbackManagerAPI { + private final KnockbackManager plugin; + + public KnockbackManagerAPIImpl(KnockbackManager plugin) { + this.plugin = plugin; + } + + @Override + public boolean isMisplacing(Player player) { + PlayerKnockbackData data = getPlayerData(player); + FileConfiguration config = getKBConfig(data.getCurrentKBFile()); + + return config != null && + plugin.getServerTick() - data.getLastHitTick() < config.getInt("misplace.delay"); + } + + @Override + public boolean isFilter(Player player) { + return getPlayerData(player).isFiltered(); + } + + @Override + public FileConfiguration getKBConfig(String configName) { + if (!isKBFileExist(configName)) { + return null; + } + + ConfigHolder holder = plugin.getConfigManager().getConfigs().get(configName); + return (FileConfiguration) holder.getConfig(); + } + + @Override + public String getKBFile(Player player) { + return getPlayerData(player).getCurrentKBFile(); + } + + @Override + public boolean setKBFile(Player player, String configName) { + if (!isKBFileExist(configName)) { + return false; + } + + getPlayerData(player).setCurrentKBFile(configName); + return true; + } + + @Override + public boolean isKBFileExist(String configName) { + return plugin.getConfigManager().getConfigs().containsKey(configName); + } + + @Override + public void setFilter(Player player, boolean filtered) { + getPlayerData(player).setFiltered(filtered); + } + + private PlayerKnockbackData getPlayerData(Player player) { + return plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/api/KBMPlayerVelocityEvent.java b/src/main/java/me/dw1e/kbm/api/KBMPlayerVelocityEvent.java new file mode 100644 index 0000000..965dfbd --- /dev/null +++ b/src/main/java/me/dw1e/kbm/api/KBMPlayerVelocityEvent.java @@ -0,0 +1,46 @@ +package me.dw1e.kbm.api; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.util.Vector; + +public class KBMPlayerVelocityEvent extends PlayerEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private Vector velocity; + private boolean cancelled; + + public KBMPlayerVelocityEvent(Player player, Vector velocity) { + super(player); + this.velocity = velocity; + this.cancelled = false; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public Vector getVelocity() { + return this.velocity; + } + + public void setVelocity(Vector velocity) { + this.velocity = velocity; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/api/KnockbackManagerAPI.java b/src/main/java/me/dw1e/kbm/api/KnockbackManagerAPI.java new file mode 100644 index 0000000..e0d08c3 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/api/KnockbackManagerAPI.java @@ -0,0 +1,14 @@ +package me.dw1e.kbm.api; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +public interface KnockbackManagerAPI { + boolean isMisplacing(Player player); + boolean isFilter(Player player); + FileConfiguration getKBConfig(String configName); + String getKBFile(Player player); + boolean setKBFile(Player player, String configName); + boolean isKBFileExist(String configName); + void setFilter(Player player, boolean filtered); +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/commands/KBMCommandHandler.java b/src/main/java/me/dw1e/kbm/commands/KBMCommandHandler.java new file mode 100644 index 0000000..dee8733 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/commands/KBMCommandHandler.java @@ -0,0 +1,491 @@ +package me.dw1e.kbm.commands; + +import me.dw1e.kbm.KnockbackManager; +import me.dw1e.kbm.config.ConfigHolder; +import me.dw1e.kbm.data.PlayerKnockbackData; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +public class KBMCommandHandler implements TabExecutor { + private final KnockbackManager plugin; + private final List editableConfigPaths; + + public KBMCommandHandler(KnockbackManager plugin) { + this.plugin = plugin; + this.editableConfigPaths = Arrays.asList( + "horizontal.ground", "horizontal.air", "horizontal.sprint_extra", + "horizontal.projectile_multiplier", "vertical.ground", "vertical.air", + "vertical.sprint_extra", "vertical.projectile_multiplier", + "misplace.enabled", "misplace.delay", "stop_sprint", "y_limit", "hit_delay" + ); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("kbm.use")) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "你没有此命令的使用权限!"); + return true; + } + + if (args.length == 0) { + sendHelp(sender, label); + return true; + } + + String subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case "help": + sendHelp(sender, label); + break; + case "create": + handleCreate(sender, args, label); + break; + case "delete": + handleDelete(sender, args, label); + break; + case "list": + handleList(sender); + break; + case "edit": + handleEdit(sender, args, label); + break; + case "view": + handleView(sender, args, label); + break; + case "reload": + handleReload(sender, args, label); + break; + case "getkb": + handleGetKB(sender, args, label); + break; + case "setkb": + handleSetKB(sender, args, label); + break; + case "filter": + handleFilter(sender, args, label); + break; + default: + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "未知子命令: " + args[0]); + break; + } + + return true; + } + + private void sendHelp(CommandSender sender, String label) { + String commandBase = ChatColor.WHITE + " /" + label; + + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + "可用命令:"); + sender.sendMessage(commandBase + " create" + ChatColor.GRAY + ": 创建KB文件"); + sender.sendMessage(commandBase + " delete" + ChatColor.GRAY + ": 删除KB文件"); + sender.sendMessage(commandBase + " list" + ChatColor.GRAY + ": 查看已读取的KB文件"); + sender.sendMessage(commandBase + " edit" + ChatColor.GRAY + ": 编辑KB文件的数值"); + sender.sendMessage(commandBase + " view" + ChatColor.GRAY + ": 查看KB文件的数值"); + sender.sendMessage(commandBase + " reload" + ChatColor.GRAY + ": 重新加载KB文件"); + sender.sendMessage(commandBase + " getkb" + ChatColor.GRAY + ": 查看玩家使用的KB文件"); + sender.sendMessage(commandBase + " setkb" + ChatColor.GRAY + ": 设置玩家使用的KB文件"); + sender.sendMessage(commandBase + " filter" + ChatColor.GRAY + ": 过滤玩家"); + sender.sendMessage(ChatColor.DARK_GREEN + "使用教程&参考配置: 请见群(673765463)文件"); + } + + private void handleCreate(CommandSender sender, String[] args, String label) { + if (args.length != 2) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " create "); + return; + } + plugin.getConfigManager().createConfig(args[1], sender); + } + + private void handleDelete(CommandSender sender, String[] args, String label) { + if (args.length != 2) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " delete "); + return; + } + + String configName = args[1]; + if (configName.equals("default")) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "默认KB文件不允许删除!"); + return; + } + + if (!plugin.getConfigManager().getConfigs().containsKey(configName)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "KB文件 " + configName + " 不存在!"); + return; + } + + + plugin.getPlayerDataManager().getAllData().values().stream() + .filter(data -> data.getCurrentKBFile().equals(configName)) + .forEach(data -> data.setCurrentKBFile("default")); + + + plugin.getConfigManager().getConfigs().remove(configName); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + "删除 " + configName + " 成功!"); + } + + private void handleList(CommandSender sender) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + "已读取的KB文件:"); + plugin.getConfigManager().getConfigs().keySet().forEach(configName -> + sender.sendMessage(" " + ChatColor.WHITE + configName) + ); + } + + private void handleEdit(CommandSender sender, String[] args, String label) { + if (args.length != 4) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " edit <配置路径> <数值>"); + return; + } + + String configName = args[1]; + String configPath = args[2].toLowerCase(); + String value = args[3]; + + if (!plugin.getConfigManager().getConfigs().containsKey(configName)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "KB文件 " + configName + " 不存在!"); + return; + } + + if (!editableConfigPaths.contains(configPath)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "无效的配置路径: " + configPath); + return; + } + + ConfigHolder holder = plugin.getConfigManager().getConfigs().get(configName); + FileConfiguration config = (FileConfiguration) holder.getConfig(); + + try { + Object oldValue = config.get(configPath); + Object newValue = parseValue(configPath, value); + + if (oldValue != null && oldValue.equals(newValue)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无变化. " + configName + " 中的 " + configPath + " 原已设置为 " + oldValue); + return; + } + + config.set(configPath, newValue); + config.save(holder.getFile()); + + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "已将 " + ChatColor.WHITE + configName + ChatColor.GRAY + " 中的 " + + ChatColor.WHITE + configPath + ChatColor.GRAY + " 设置为 " + + ChatColor.WHITE + newValue); + + } catch (Exception e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "设置配置值时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void handleView(CommandSender sender, String[] args, String label) { + if (args.length != 2) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " view "); + return; + } + + String configName = args[1]; + if (!plugin.getConfigManager().getConfigs().containsKey(configName)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "KB文件 " + configName + " 不存在!"); + return; + } + + ConfigHolder holder = plugin.getConfigManager().getConfigs().get(configName); + FileConfiguration config = (FileConfiguration) holder.getConfig(); + + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "查看KB文件 " + ChatColor.WHITE + configName + ChatColor.GRAY + " 的数值:"); + + StringBuilder configDisplay = new StringBuilder(); + formatConfigSection(configDisplay, config, 1); + + String[] lines = configDisplay.toString().split("\n"); + for (String line : lines) { + sender.sendMessage(ChatColor.YELLOW + line); + } + } + + private void handleReload(CommandSender sender, String[] args, String label) { + if (args.length != 2) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " reload "); + return; + } + + String configName = args[1]; + if (configName.equals("*")) { + plugin.getConfigManager().loadConfigs(sender); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + "已重载所有KB文件!"); + } else { + plugin.getConfigManager().reloadConfig(configName, sender); + } + } + + private void handleGetKB(CommandSender sender, String[] args, String label) { + if (args.length != 2) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " getkb <玩家>"); + return; + } + + Player player = Bukkit.getPlayerExact(args[1]); + if (player == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "没有找到名为 " + args[1] + " 的玩家!"); + return; + } + + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + if (data == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无法获取玩家 " + player.getName() + " 的数据!"); + return; + } + + if (data.isFiltered()) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + player.getName() + " 当前位于过滤名单中!"); + } else { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.WHITE + + player.getName() + ChatColor.GRAY + " 当前使用的KB为: " + + ChatColor.WHITE + data.getCurrentKBFile()); + } + } + + private void handleSetKB(CommandSender sender, String[] args, String label) { + if (args.length != 3) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " setkb <玩家|*> "); + return; + } + + String playerName = args[1]; + String configName = args[2]; + + if (!plugin.getConfigManager().getConfigs().containsKey(configName)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "KB文件 " + configName + " 不存在!"); + return; + } + + if (playerName.equals("*")) { + // 设置所有玩家 + plugin.getPlayerDataManager().getAllData().values().forEach(data -> + data.setCurrentKBFile(configName) + ); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "已将所有玩家使用的KB设置为 " + ChatColor.WHITE + configName); + } else { + // 设置单个玩家 + Player player = Bukkit.getPlayerExact(playerName); + if (player == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "没有找到名为 " + playerName + " 的玩家!"); + return; + } + + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + if (data == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无法获取玩家 " + player.getName() + " 的数据!"); + return; + } + + if (data.getCurrentKBFile().equals(configName)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无变化. " + player.getName() + " 原已使用 " + configName); + return; + } + + data.setCurrentKBFile(configName); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "已将 " + ChatColor.WHITE + player.getName() + ChatColor.GRAY + + " 使用的KB设置为 " + ChatColor.WHITE + configName); + } + } + + private void handleFilter(CommandSender sender, String[] args, String label) { + if (args.length != 3) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "用法: /" + label + " filter <玩家> "); + return; + } + + Player player = Bukkit.getPlayerExact(args[1]); + if (player == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "没有找到名为 " + args[1] + " 的玩家!"); + return; + } + + String filterValue = args[2].toLowerCase(); + if (!filterValue.equals("true") && !filterValue.equals("false")) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无效的布尔值: " + args[2]); + return; + } + + boolean filtered = Boolean.parseBoolean(filterValue); + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + if (data == null) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无法获取玩家 " + player.getName() + " 的数据!"); + return; + } + + if (data.isFiltered() == filtered) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "无变化. " + player.getName() + " 原已" + (filtered ? "加入" : "移出") + "过滤名单!"); + return; + } + + data.setFiltered(filtered); + String action = filtered ? ChatColor.GREEN + "加入" : ChatColor.RED + "移出"; + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "已将 " + ChatColor.WHITE + player.getName() + " " + action + + ChatColor.GRAY + "过滤名单!"); + } + + + private Object parseValue(String configPath, String value) { + // 处理布尔值 + if (configPath.equals("misplace.enabled") || configPath.equals("stop_sprint")) { + if (value.equalsIgnoreCase("true") || value.equals("1")) { + return true; + } else if (value.equalsIgnoreCase("false") || value.equals("0")) { + return false; + } + throw new IllegalArgumentException("无效的布尔值: " + value); + } + + + if (configPath.equals("misplace.delay") || configPath.equals("hit_delay")) { + try { + int intValue = Integer.parseInt(value); + if (configPath.equals("misplace.delay") && (intValue < 1 || intValue > 5)) { + throw new IllegalArgumentException("misplace.delay 必须在 1-5 范围内"); + } + return intValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("无效的整数值: " + value); + } + } + + + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("无效的数值: " + value); + } + } + + private void formatConfigSection(StringBuilder builder, ConfigurationSection section, int depth) { + StringBuilder indent = new StringBuilder(); + for (int i = 0; i < depth; i++) { + indent.append(" "); + } + + for (String key : section.getKeys(false)) { + if (section.isConfigurationSection(key)) { + builder.append(ChatColor.YELLOW).append(indent).append(key) + .append(ChatColor.WHITE).append(":\n"); + formatConfigSection(builder, section.getConfigurationSection(key), depth + 1); + } else { + Object value = section.get(key); + builder.append(ChatColor.GRAY).append(indent).append(key) + .append(ChatColor.WHITE).append(": ").append(value).append("\n"); + } + } + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List completions = new ArrayList<>(); + if (!sender.hasPermission("kbm.use")) return completions; + + if (args.length == 1) { + return filterCompletions(Arrays.asList( + "create", "delete", "list", "edit", "view", "reload", "getkb", "setkb", "filter", "help" + ), args[0]); + } + + List configNames = new ArrayList<>(plugin.getConfigManager().getConfigs().keySet()); + List playerNames = Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()); + + String subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case "delete": + case "edit": + case "view": + case "reload": + if (args.length == 2) { + return filterCompletions(configNames, args[1]); + } + if (args.length == 3 && subCommand.equals("edit")) { + return filterCompletions(editableConfigPaths, args[2]); + } + if (args.length == 4 && subCommand.equals("edit")) { + + String configPath = args[2].toLowerCase(); + if (configPath.equals("misplace.enabled") || configPath.equals("stop_sprint")) { + return filterCompletions(Arrays.asList("true", "false"), args[3]); + } else if (configPath.equals("misplace.delay")) { + return filterCompletions(Arrays.asList("1", "2", "3", "4", "5"), args[3]); + } + } + break; + + case "getkb": + case "setkb": + case "filter": + if (args.length == 2) { + List allOptions = new ArrayList<>(playerNames); + allOptions.add("*"); + return filterCompletions(allOptions, args[1]); + } + if (args.length == 3) { + if (subCommand.equals("filter")) { + return filterCompletions(Arrays.asList("true", "false"), args[2]); + } else if (subCommand.equals("setkb")) { + return filterCompletions(configNames, args[2]); + } + } + break; + + case "create": + if (args.length == 2) { + + return filterCompletions(Arrays.asList("custom", "new", "test"), args[1]); + } + break; + } + + return completions; + } + + private List filterCompletions(List options, String input) { + return options.stream() + .filter(option -> option.toLowerCase().startsWith(input.toLowerCase())) + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/config/ConfigHolder.java b/src/main/java/me/dw1e/kbm/config/ConfigHolder.java new file mode 100644 index 0000000..770c000 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/config/ConfigHolder.java @@ -0,0 +1,21 @@ +package me.dw1e.kbm.config; + +import java.io.File; + +public class ConfigHolder { + private final File configFile; + private final Object config; + + public ConfigHolder(File configFile, Object config) { + this.configFile = configFile; + this.config = config; + } + + public File getFile() { + return this.configFile; + } + + public Object getConfig() { + return this.config; + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/config/ConfigManager.java b/src/main/java/me/dw1e/kbm/config/ConfigManager.java new file mode 100644 index 0000000..5850ab8 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/config/ConfigManager.java @@ -0,0 +1,242 @@ +package me.dw1e.kbm.config; + +import me.dw1e.kbm.KnockbackManager; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class ConfigManager { + private final KnockbackManager plugin; + private final Map configs; + private final FileConfiguration defaultConfig; + + public ConfigManager(KnockbackManager plugin) { + this.plugin = plugin; + this.configs = new HashMap<>(); + + this.defaultConfig = YamlConfiguration.loadConfiguration( + new InputStreamReader(plugin.getResource("default.yml")) + ); + } + + public void loadConfigs(CommandSender sender) { + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + File defaultFile = new File(plugin.getDataFolder(), "default.yml"); + if (!defaultFile.exists()) { + plugin.saveResource("default.yml", false); + } + + configs.clear(); + + try { + Files.list(Paths.get(plugin.getDataFolder().getPath())) + .filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".yml")) + .forEach(path -> { + String fileName = path.getFileName().toString().replace(".yml", ""); + File file = path.toFile(); + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + + configs.put(fileName, new ConfigHolder(file, config)); + updateConfigValues(fileName, config, sender); + + try { + config.save(file); + } catch (IOException e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "保存 " + fileName + " 失败!"); + e.printStackTrace(); + } + }); + } catch (IOException e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "读取配置文件时发生错误!"); + e.printStackTrace(); + } + } + + private void updateConfigValues(String configName, FileConfiguration config, CommandSender sender) { + if (config.get("ground_horizontal") != null) { + plugin.logMessage(ChatColor.GREEN + "已将旧版配置文件 " + configName + " 更新!"); + + migrateOldConfig(config); + } + + config.options().copyDefaults(true).copyHeader(true); + config.addDefaults(defaultConfig); + + config.set("misplace.delay", Math.max(1, Math.min(5, config.getInt("misplace.delay")))); + } + + private void migrateOldConfig(FileConfiguration config) { + + config.set("horizontal.ground", config.get("ground_horizontal")); + config.set("horizontal.air", config.get("air_horizontal")); + config.set("horizontal.sprint_extra", config.get("extra_horizontal")); + config.set("horizontal.projectile_multiplier", config.get("projectile_multiplier_horizontal")); + + + config.set("vertical.ground", config.get("ground_vertical")); + config.set("vertical.air", config.get("air_vertical")); + config.set("vertical.sprint_extra", config.get("extra_vertical")); + config.set("vertical.projectile_multiplier", config.get("projectile_multiplier_vertical")); + + + String[] oldKeys = { + "ground_horizontal", "air_horizontal", "extra_horizontal", "projectile_multiplier_horizontal", + "ground_vertical", "air_vertical", "extra_vertical", "projectile_multiplier_vertical" + }; + + for (String key : oldKeys) { + config.set(key, null); + } + } + + public Map getConfigs() { + return configs; + } + + public void createConfig(String name, CommandSender sender) { + if (configs.containsKey(name)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "配置文件 " + name + " 已存在!"); + return; + } + + File file = new File(plugin.getDataFolder(), name + ".yml"); + try { + if (!file.createNewFile()) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "创建配置文件 " + name + " 失败!"); + return; + } + } catch (IOException e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "创建配置文件 " + name + " 失败!"); + e.printStackTrace(); + return; + } + + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + config.options().copyDefaults(true).copyHeader(true); + config.addDefaults(defaultConfig); + + try { + config.save(file); + } catch (IOException e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "保存配置文件 " + name + " 失败!"); + e.printStackTrace(); + return; + } + + configs.put(name, new ConfigHolder(file, config)); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + + "创建配置文件 " + name + " 成功!"); + } + + public void reloadConfig(String name, CommandSender sender) { + if (!configs.containsKey(name)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "配置文件 " + name + " 不存在!"); + return; + } + + ConfigHolder holder = configs.get(name); + try { + ((FileConfiguration) holder.getConfig()).load(holder.getFile()); + updateConfigValues(name, (FileConfiguration) holder.getConfig(), sender); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + + "重载配置文件 " + name + " 成功!"); + } catch (Exception e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + + "重载配置文件 " + name + " 失败!"); + e.printStackTrace(); + } + } + + + public void deleteConfig(String name, CommandSender sender) { + if (name.equals("default")) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "默认KB文件不允许删除!"); + return; + } + + if (!configs.containsKey(name)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "KB文件 " + name + " 不存在!"); + return; + } + + + plugin.getPlayerDataManager().getAllData().values().stream() + .filter(data -> data.getCurrentKBFile().equals(name)) + .forEach(data -> data.setCurrentKBFile("default")); + + ConfigHolder holder = configs.get(name); + File configFile = holder.getFile(); + + boolean deleted = configFile.delete(); + configs.remove(name); + + if (deleted) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + "删除 " + name + " 成功!"); + } else { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "删除 " + name + " 失败!"); + } + } + + public void saveConfig(String name, CommandSender sender) { + if (!configs.containsKey(name)) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "配置文件 " + name + " 不存在!"); + return; + } + + ConfigHolder holder = configs.get(name); + try { + ((FileConfiguration) holder.getConfig()).save(holder.getFile()); + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.GREEN + "保存配置文件 " + name + " 成功!"); + } catch (IOException e) { + sender.sendMessage(plugin.getPluginPrefix() + ChatColor.RED + "保存配置文件 " + name + " 失败!"); + e.printStackTrace(); + } + } + + + private void validateConfigValues(String configName, FileConfiguration config) { + + int misplaceDelay = config.getInt("misplace.delay"); + if (misplaceDelay < 1 || misplaceDelay > 5) { + plugin.logMessage(ChatColor.YELLOW + "配置 " + configName + " 中的 misplace.delay 超出范围 (1-5),已自动修正"); + config.set("misplace.delay", Math.max(1, Math.min(5, misplaceDelay))); + } + + + String[] requiredPaths = { + "horizontal.ground", "horizontal.air", "vertical.ground", "vertical.air" + }; + + for (String path : requiredPaths) { + if (!config.contains(path)) { + plugin.logMessage(ChatColor.YELLOW + "配置 " + configName + " 缺少必需字段: " + path + ",使用默认值"); + config.set(path, defaultConfig.get(path)); + } + } + } + + + public FileConfiguration getDefaultConfig() { + return this.defaultConfig; + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/data/PlayerDataManager.java b/src/main/java/me/dw1e/kbm/data/PlayerDataManager.java new file mode 100644 index 0000000..1012a26 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/data/PlayerDataManager.java @@ -0,0 +1,40 @@ +package me.dw1e.kbm.data; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class PlayerDataManager { + private final Map playerData; + + public PlayerDataManager() { + this.playerData = new ConcurrentHashMap<>(); + } + + public void registerAllPlayers() { + Bukkit.getOnlinePlayers().forEach(this::registerPlayer); + } + + public void registerPlayer(Player player) { + playerData.putIfAbsent(player.getUniqueId(), new PlayerKnockbackData(player)); + } + + public void unregisterPlayer(UUID uuid) { + playerData.remove(uuid); + } + + public PlayerKnockbackData getPlayerData(UUID uuid) { + return playerData.get(uuid); + } + + public Map getAllData() { + return playerData; + } + + public void clearAllData() { + playerData.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/data/PlayerKnockbackData.java b/src/main/java/me/dw1e/kbm/data/PlayerKnockbackData.java new file mode 100644 index 0000000..825218f --- /dev/null +++ b/src/main/java/me/dw1e/kbm/data/PlayerKnockbackData.java @@ -0,0 +1,103 @@ +package me.dw1e.kbm.data; + +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +public class PlayerKnockbackData { + private Vector pendingVelocity; + private boolean isOnGround; + private boolean isFiltered; + private String currentKBFile; + private int lastAttackerId; + private double lastGroundY; + private final Player player; + private int lastHitTick; + private int hitDelay; + private int serverTick; + + public void setServerTick(int serverTick) { + this.serverTick = serverTick; + } + + public int getLastAttackerId() { + return this.lastAttackerId; + } + + public int getLastHitTick() { + return this.lastHitTick; + } + + public int getHitDelay() { + return this.hitDelay; + } + + public boolean isOnGround() { + return this.isOnGround; + } + + public boolean isFiltered() { + return this.isFiltered; + } + + public void setHitDelay(int hitDelay) { + this.hitDelay = hitDelay; + } + + public void setLastHitTick(int lastHitTick) { + this.lastHitTick = lastHitTick; + } + + public void setPendingVelocity(Vector velocity) { + this.pendingVelocity = velocity; + } + + public int getServerTick() { + return this.serverTick; + } + + public Vector getPendingVelocity() { + return this.pendingVelocity; + } + + public void setLastAttackerId(int lastAttackerId) { + this.lastAttackerId = lastAttackerId; + } + + public double getLastGroundY() { + return this.lastGroundY; + } + + public void setLastGroundY(double lastGroundY) { + this.lastGroundY = lastGroundY; + } + + public boolean checkOnGround() { + return this.player.isOnGround(); + } + + public void setSprinting(boolean sprinting) { + this.isOnGround = sprinting; + } + + public PlayerKnockbackData(Player player) { + this.currentKBFile = "default"; + this.pendingVelocity = null; + this.lastAttackerId = -1; + this.player = player; + this.lastGroundY = player.getLocation().getY(); + } + + public String getCurrentKBFile() { + return this.currentKBFile; + } + + public void setFiltered(boolean filtered) { + this.isFiltered = filtered; + // 设置无敌时间 + this.player.setMaximumNoDamageTicks(20); + } + + public void setCurrentKBFile(String kbFile) { + this.currentKBFile = kbFile; + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/knockback/KnockbackCalculator.java b/src/main/java/me/dw1e/kbm/knockback/KnockbackCalculator.java new file mode 100644 index 0000000..56530e2 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/knockback/KnockbackCalculator.java @@ -0,0 +1,158 @@ +package me.dw1e.kbm.knockback; + +import me.dw1e.kbm.KnockbackManager; +import me.dw1e.kbm.api.KBMPlayerVelocityEvent; +import me.dw1e.kbm.config.ConfigHolder; +import me.dw1e.kbm.data.PlayerKnockbackData; +import me.dw1e.kbm.data.PlayerDataManager; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.util.Vector; + +public class KnockbackCalculator { + private final KnockbackManager plugin; + + public KnockbackCalculator(KnockbackManager plugin) { + this.plugin = plugin; + } + + public void calculateKnockback(Player victim, Player attacker, + PlayerKnockbackData victimData, Entity damager) { + PlayerKnockbackData attackerData = plugin.getPlayerDataManager() + .getPlayerData(attacker.getUniqueId()); + + if (attackerData == null) return; + + int currentTick = plugin.getServerTick(); + + victimData.setLastAttackerId(attacker.getEntityId()); + victimData.setHitDelay(currentTick); + attackerData.setLastHitTick(currentTick); + + FileConfiguration config = getVictimConfig(victimData); + if (config == null) return; + + Vector knockback = calculateBaseKnockback(victim, attacker, config, damager); + + applyKnockbackModifiers(knockback, victim, attacker, config, damager, victimData); + + KBMPlayerVelocityEvent velocityEvent = new KBMPlayerVelocityEvent(victim, knockback); + Bukkit.getPluginManager().callEvent(velocityEvent); + + if (!velocityEvent.isCancelled()) { + int hitDelay = config.getInt("hit_delay"); + if (victim.getMaximumNoDamageTicks() != hitDelay) { + victim.setMaximumNoDamageTicks(hitDelay); + } + + victimData.setPendingVelocity(velocityEvent.getVelocity()); + } + } + + private FileConfiguration getVictimConfig(PlayerKnockbackData victimData) { + ConfigHolder holder = plugin.getConfigManager().getConfigs() + .get(victimData.getCurrentKBFile()); + return holder != null ? (FileConfiguration) holder.getConfig() : null; + } + + private Vector calculateBaseKnockback(Player victim, Player attacker, + FileConfiguration config, Entity damager) { + Vector direction = calculateDirectionVector(victim, attacker); + + boolean victimOnGround = victim.isOnGround(); + double horizontal = victimOnGround ? + config.getDouble("horizontal.ground") : config.getDouble("horizontal.air"); + double vertical = victimOnGround ? + config.getDouble("vertical.ground") : config.getDouble("vertical.air"); + + Vector baseKnockback = direction.multiply(new Vector(horizontal, vertical, horizontal)); + + if (damager instanceof Projectile) { + double projectileMultiplier = config.getDouble("horizontal.projectile_multiplier"); + baseKnockback.multiply(new Vector(projectileMultiplier, + config.getDouble("vertical.projectile_multiplier"), projectileMultiplier)); + } + + return baseKnockback; + } + + private Vector calculateDirectionVector(Player victim, Player attacker) { + double x = victim.getLocation().getX() - attacker.getLocation().getX(); + double z = victim.getLocation().getZ() - attacker.getLocation().getZ(); + + if (Math.hypot(x, z) < 1.0E-4D) { + x = (Math.random() - Math.random()) * 0.01D; + z = (Math.random() - Math.random()) * 0.01D; + } + + float attackerYaw = attacker.getLocation().getYaw() * 3.1415927F / 180.0F; + + return new Vector(-Math.sin(attackerYaw), 0.0D, Math.cos(attackerYaw)) + .normalize().setY(1.0D); + } + + private void applyKnockbackModifiers(Vector knockback, Player victim, Player attacker, + FileConfiguration config, Entity damager, + PlayerKnockbackData victimData) { + if (!(damager instanceof Projectile)) { + applyKnockbackEnchantment(knockback, attacker, config); + } else if (damager instanceof Arrow) { + applyArrowKnockback(knockback, (Arrow) damager, config); + } + + + double yLimit = config.getDouble("y_limit"); + if (victim.getLocation().getY() - victimData.getLastGroundY() > yLimit) { + knockback.setY(0.0D); + } + } + + private void applyKnockbackEnchantment(Vector knockback, Player attacker, + FileConfiguration config) { + int knockbackLevel = attacker.getItemInHand().getEnchantmentLevel(Enchantment.KNOCKBACK); + + boolean stopSprint = config.getBoolean("stop_sprint"); + PlayerKnockbackData attackerData = plugin.getPlayerDataManager() + .getPlayerData(attacker.getUniqueId()); + + if ((!stopSprint && attackerData.isOnGround()) || + (stopSprint && attacker.isSprinting())) { + knockbackLevel++; + } + + if (knockbackLevel > 0) { + float attackerYaw = attacker.getLocation().getYaw() * 3.1415927F / 180.0F; + double sprintExtra = config.getDouble("horizontal.sprint_extra"); + double verticalExtra = config.getDouble("vertical.sprint_extra"); + + Vector extraKnockback = new Vector( + -Math.sin(attackerYaw) * knockbackLevel * sprintExtra, + verticalExtra, + Math.cos(attackerYaw) * knockbackLevel * sprintExtra + ); + + knockback.add(extraKnockback); + } + } + + private void applyArrowKnockback(Vector knockback, Arrow arrow, FileConfiguration config) { + int arrowKnockback = arrow.getKnockbackStrength(); + if (arrowKnockback > 0) { + double knockbackStrength = arrowKnockback * 0.6; + double horizontalMagnitude = Math.hypot(knockback.getX(), knockback.getZ()); + + Vector extraKnockback = new Vector( + knockback.getX() * knockbackStrength / horizontalMagnitude, + config.getDouble("vertical.sprint_extra"), + knockback.getZ() * knockbackStrength / horizontalMagnitude + ); + + knockback.add(extraKnockback); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/listeners/PacketListener.java b/src/main/java/me/dw1e/kbm/listeners/PacketListener.java new file mode 100644 index 0000000..ac7ae92 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/listeners/PacketListener.java @@ -0,0 +1,106 @@ +package me.dw1e.kbm.listeners; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import me.dw1e.kbm.KnockbackManager; +import me.dw1e.kbm.config.ConfigHolder; +import me.dw1e.kbm.data.PlayerKnockbackData; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.List; + +public class PacketListener extends PacketAdapter { + private final KnockbackManager plugin; + private static final List LISTENED_PACKETS = Arrays.asList( + PacketType.Play.Server.ENTITY_TELEPORT, + PacketType.Play.Server.REL_ENTITY_MOVE_LOOK, + PacketType.Play.Server.REL_ENTITY_MOVE, + PacketType.Play.Server.ENTITY_LOOK + ); + + public PacketListener(KnockbackManager plugin) { + super(plugin, ListenerPriority.NORMAL, LISTENED_PACKETS); + this.plugin = plugin; + } + + public void register() { + ProtocolLibrary.getProtocolManager().addPacketListener(this); + } + + public void unregister() { + ProtocolLibrary.getProtocolManager().removePacketListener(this); + } + + @Override + public void onPacketSending(PacketEvent event) { + if (!LISTENED_PACKETS.contains(event.getPacketType())) return; + + Player player = event.getPlayer(); + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + + if (data == null) return; + + FileConfiguration config = getPlayerConfig(data); + if (config == null || !config.getBoolean("misplace.enabled")) return; + + handleMisplaceCheck(event, data, player, config); + } + + private void handleMisplaceCheck(PacketEvent event, PlayerKnockbackData data, + Player player, FileConfiguration config) { + int entityId = event.getPacket().getIntegers().read(0); + int currentTick = plugin.getServerTick(); + + // 检查是否是需要防Misplace的实体 + if (entityId == data.getLastAttackerId() && + currentTick - data.getLastHitTick() <= player.getMaximumNoDamageTicks()) { + + Player attacker = findPlayerById(entityId); + if (attacker != null && shouldDelayPacket(data, attacker, currentTick, config)) { + delayPacket(event, data, player, config); + } + } + } + + private Player findPlayerById(int entityId) { + return Bukkit.getOnlinePlayers().stream() + .filter(p -> p.getEntityId() == entityId) + .findFirst() + .orElse(null); + } + + private boolean shouldDelayPacket(PlayerKnockbackData data, Player attacker, + int currentTick, FileConfiguration config) { + PlayerKnockbackData attackerData = plugin.getPlayerDataManager() + .getPlayerData(attacker.getUniqueId()); + + return attackerData != null && + currentTick - data.getHitDelay() > attacker.getMaximumNoDamageTicks(); + } + + private void delayPacket(PacketEvent event, PlayerKnockbackData data, + Player player, FileConfiguration config) { + PacketContainer packet = event.getPacket().shallowClone(); + event.setCancelled(true); + + data.setServerTick(plugin.getServerTick()); + + int delayTicks = config.getInt("misplace.delay"); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet, false); + }, delayTicks); + } + + private FileConfiguration getPlayerConfig(PlayerKnockbackData data) { + ConfigHolder holder = plugin.getConfigManager().getConfigs() + .get(data.getCurrentKBFile()); + return holder != null ? (FileConfiguration) holder.getConfig() : null; + } +} \ No newline at end of file diff --git a/src/main/java/me/dw1e/kbm/listeners/PlayerEventListener.java b/src/main/java/me/dw1e/kbm/listeners/PlayerEventListener.java new file mode 100644 index 0000000..10ce634 --- /dev/null +++ b/src/main/java/me/dw1e/kbm/listeners/PlayerEventListener.java @@ -0,0 +1,157 @@ +package me.dw1e.kbm.listeners; + +import me.dw1e.kbm.KnockbackManager; +import me.dw1e.kbm.api.KBMPlayerVelocityEvent; +import me.dw1e.kbm.config.ConfigHolder; +import me.dw1e.kbm.data.PlayerKnockbackData; +import me.dw1e.kbm.knockback.KnockbackCalculator; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.player.*; +import org.bukkit.projectiles.ProjectileSource; +import org.bukkit.util.Vector; + +public class PlayerEventListener implements Listener { + private final KnockbackManager plugin; + private final KnockbackCalculator knockbackCalculator; + + public PlayerEventListener(KnockbackManager plugin) { + this.plugin = plugin; + this.knockbackCalculator = new KnockbackCalculator(plugin); + } + + @EventHandler + private void onPlayerMove(PlayerMoveEvent event) { + Location to = event.getTo(); + Location from = event.getFrom(); + + if (to.equals(from)) return; + + Player player = event.getPlayer(); + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + + if (data != null && data.checkOnGround()) { + data.setLastGroundY(to.getY()); + } + } + + @EventHandler + private void onPlayerToggleSprint(PlayerToggleSprintEvent event) { + PlayerKnockbackData data = plugin.getPlayerDataManager() + .getPlayerData(event.getPlayer().getUniqueId()); + + if (data != null) { + data.setSprinting(event.isSprinting()); + } + } + + @EventHandler + private void onPlayerCommand(PlayerCommandPreprocessEvent event) { + String message = event.getMessage().toLowerCase(); + + // 支持的命令列表 + String[] supportedCommands = { + "/knockbackmanager:kbm", "/kb", "/kbm", "/knockback", + "/knockbackmanager:kb", "/knockbackmanager:knockback" + }; + + for (String cmd : supportedCommands) { + if (message.equals(cmd)) { + event.getPlayer().sendMessage(plugin.getPluginPrefix() + ChatColor.GRAY + + "此服务器正在使用 " + ChatColor.WHITE + "Knockback Manager" + + ChatColor.GRAY + "(v" + plugin.getDescription().getVersion() + + ") 击退修改插件"); + break; + } + } + } + + @EventHandler + private void onPlayerQuit(PlayerQuitEvent event) { + plugin.getPlayerDataManager().unregisterPlayer(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onPlayerVelocity(PlayerVelocityEvent event) { + Player player = event.getPlayer(); + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + + if (data != null && data.getPendingVelocity() != null) { + event.setVelocity(data.getPendingVelocity()); + data.setPendingVelocity(null); + } + } + + @EventHandler + private void onPlayerJoin(PlayerJoinEvent event) { + plugin.getPlayerDataManager().registerPlayer(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + private void onEntityDamageByEntity(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player)) return; + + Entity damager = event.getDamager(); + Player victim = (Player) event.getEntity(); + PlayerKnockbackData victimData = plugin.getPlayerDataManager().getPlayerData(victim.getUniqueId()); + + if (victimData == null || victimData.isFiltered()) return; + + // 处理攻击者 + Player attacker = extractAttacker(damager); + if (attacker == null) return; + + // 检查自伤 + boolean isSelfDamage = attacker.equals(victim); + if (isSelfDamage) return; + + // 处理击退计算 + knockbackCalculator.calculateKnockback(victim, attacker, victimData, damager); + } + + private Player extractAttacker(Entity damager) { + if (damager instanceof Player) { + return (Player) damager; + } else if (damager instanceof Projectile) { + ProjectileSource shooter = ((Projectile) damager).getShooter(); + return (shooter instanceof Player) ? (Player) shooter : null; + } + return null; + } + @EventHandler(priority = EventPriority.HIGH) + private void onEntityDamage(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Player)) return; + + Player victim = (Player) event.getEntity(); + PlayerKnockbackData victimData = plugin.getPlayerDataManager().getPlayerData(victim.getUniqueId()); + + // 如果玩家被过滤,取消击退 + if (victimData != null && victimData.isFiltered()) { + // 重置击退速度 + victim.setVelocity(new Vector(0, 0, 0)); + } + } + + // 处理玩家重生事件,重置数据 + @EventHandler + private void onPlayerRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + PlayerKnockbackData data = plugin.getPlayerDataManager().getPlayerData(player.getUniqueId()); + if (data != null) { + data.setLastGroundY(player.getLocation().getY()); + data.setPendingVelocity(null); + } + } +} \ No newline at end of file diff --git a/src/main/resources/default.yml b/src/main/resources/default.yml new file mode 100644 index 0000000..049af5b --- /dev/null +++ b/src/main/resources/default.yml @@ -0,0 +1,16 @@ +horizontal: + ground: 0.4 + air: 0.4 + sprint_extra: 0.5 + projectile_multiplier: 1.0 +vertical: + ground: 0.36075 + air: 0.24775 + sprint_extra: 0.1 + projectile_multiplier: 1.0 +misplace: + enabled: false + delay: 1 +stop_sprint: true +y_limit: 0.675 +hit_delay: 20 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..1d57b31 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,18 @@ +name: KnockbackManager +main: me.dw1e.kbm.KnockbackManager +version: 1.13.4 +author: dw1e +softdepend: [ ProtocolLib ] +api-version: 1.13 + +commands: + kbm: + aliases: [ kb, knockback ] + +permissions: + kbm.use: + description: Allows usage of Knockback Manager commands + default: op + kbm.admin: + description: Full access to Knockback Manager features + default: op \ No newline at end of file