From 282bebdcdbe2d741cc7ac25083bf5a81837f28e8 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sat, 23 Nov 2024 17:51:03 +0100 Subject: [PATCH] commit --- pom.xml | 3 +- reflections.txt | 12 +- .../java/eu/m724/tweaks/TweaksConfig.java | 88 ++++++++++++ .../java/eu/m724/tweaks/TweaksPlugin.java | 49 ++++--- .../eu/m724/tweaks/chat/ChatListener.java | 62 +++++--- .../java/eu/m724/tweaks/chat/ChatManager.java | 18 ++- .../eu/m724/tweaks/chat/ChatRoomLoader.java | 4 - .../m724/tweaks/compass/CompassListener.java | 133 ++++++++++++++++++ .../m724/tweaks/compass/CompassManager.java | 15 ++ .../compass/CompassPlayerPreferences.java | 17 +++ .../m724/tweaks/door/DoorKnockListener.java | 57 ++++++++ .../java/eu/m724/tweaks/door/DoorManager.java | 18 +++ ...oorListener.java => DoorOpenListener.java} | 47 +------ .../eu/m724/tweaks/motd/MotdListener.java | 14 +- .../eu/m724/tweaks/ping/F3NameListener.java | 73 +++++++--- .../java/eu/m724/tweaks/ping/PingChecker.java | 60 ++------ .../eu/m724/tweaks/ping/PingCommands.java | 6 + .../java/eu/m724/tweaks/ping/PlayerPing.java | 19 ++- .../m724/tweaks/ping/ProtocolPingChecker.java | 66 +++++++++ .../worldborder/WorldBorderInfoPayload.java | 28 ++++ .../worldborder/WorldBorderManager.java | 59 ++++++-- src/main/resources/config.yml | 50 +++++++ 22 files changed, 706 insertions(+), 192 deletions(-) create mode 100644 src/main/java/eu/m724/tweaks/TweaksConfig.java create mode 100644 src/main/java/eu/m724/tweaks/compass/CompassListener.java create mode 100644 src/main/java/eu/m724/tweaks/compass/CompassManager.java create mode 100644 src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java create mode 100644 src/main/java/eu/m724/tweaks/door/DoorKnockListener.java create mode 100644 src/main/java/eu/m724/tweaks/door/DoorManager.java rename src/main/java/eu/m724/tweaks/door/{DoorListener.java => DoorOpenListener.java} (59%) create mode 100644 src/main/java/eu/m724/tweaks/ping/ProtocolPingChecker.java create mode 100644 src/main/java/eu/m724/tweaks/worldborder/WorldBorderInfoPayload.java create mode 100644 src/main/resources/config.yml diff --git a/pom.xml b/pom.xml index f39e19d..8d29459 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,6 @@ - org.apache.maven.plugins maven-shade-plugin @@ -56,7 +55,7 @@ - + net.md-5 specialsource-maven-plugin diff --git a/reflections.txt b/reflections.txt index 0d65a28..ac1be71 100644 --- a/reflections.txt +++ b/reflections.txt @@ -1,8 +1,4 @@ -usage of: - -reflections: -- ping/F3NameListener net.minecraft.network.protocol.common.custom.BrandPayload - -protocollib: -- ping/F3NameListener sending and intercepting CUSTOM_PAYLOADs to modify brand -- ping/PingChecker sending and intercepting COOKIE_RESPONSEs to measure ping +modules that require nms / reflections / protocollib: +- MOTD +- brand +- worldborder \ No newline at end of file diff --git a/src/main/java/eu/m724/tweaks/TweaksConfig.java b/src/main/java/eu/m724/tweaks/TweaksConfig.java new file mode 100644 index 0000000..736a93e --- /dev/null +++ b/src/main/java/eu/m724/tweaks/TweaksConfig.java @@ -0,0 +1,88 @@ +package eu.m724.tweaks; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.Plugin; + +public record TweaksConfig( + boolean isProtocolLib, + + boolean worldborderHide, + + boolean brandEnabled, + String brandText, + boolean brandShowPing, + boolean brandShowMspt, + + boolean doorEnabled, + boolean doorDoubleOpen, + boolean doorKnocking, + + boolean motdEnabled, + String motdSet, + + boolean chatEnabled, + boolean chatLocalEvents, + String chatDefaultName, + + boolean compassEnabled, + int compassWidth, + int compassPrecision +) { + public static final int CONFIG_VERSION = 1; + private static TweaksConfig config; + + public static TweaksConfig getConfig() { + return config; + } + + public static TweaksConfig load(Plugin plugin) { + plugin.saveDefaultConfig(); + FileConfiguration config = plugin.getConfig(); + + boolean isProtocolLib = plugin.getServer().getPluginManager().getPlugin("ProtocolLib") != null; + + int configVersion = config.getInt("magic number dont modify this", 0); + RuntimeException exception = new RuntimeException("Config version is %d, expected %d".formatted(configVersion, CONFIG_VERSION)); + if (configVersion == 0) { + throw exception; + } else if (configVersion > CONFIG_VERSION) { + throw new RuntimeException("Please follow update instructions", exception); + } else if (configVersion < CONFIG_VERSION) { + throw new RuntimeException("Did you downgrade the plugin? Remove config.yml and let the plugin re-create it", exception); + } + + boolean hideWorldBorder = config.getBoolean("worldborder.hide"); + + boolean brandEnabled = config.getBoolean("brand.enabled"); + String brandText = config.getString("brand.text"); + boolean brandShowPing = config.getBoolean("brand.showPing"); + boolean brandShowMspt = config.getBoolean("brand.showMspt"); + + boolean doorDoubleOpen = config.getBoolean("doors.doubleOpen"); + boolean doorKnocking = config.getBoolean("doors.knocking"); + boolean doorEnabled = doorDoubleOpen || doorKnocking; + + String motdSet = config.getString("motd.set"); + boolean motdEnabled = !(motdSet.equals("false") || motdSet.isBlank()); + + boolean chatEnabled = config.getBoolean("chat.enabled"); + boolean chatLocalEvents = config.getBoolean("chat.localEvents"); + String chatDefaultName = config.getString("chat.defaultName"); + + boolean compassEnabled = config.getBoolean("compass.enabled"); + int compassWidth = config.getInt("compass.width"); + int compassPrecision = config.getInt("compass.precision"); + + TweaksConfig.config = new TweaksConfig( + isProtocolLib, + hideWorldBorder, + brandEnabled, brandText, brandShowPing, brandShowMspt, + doorEnabled, doorDoubleOpen, doorKnocking, + motdEnabled, motdSet, + chatEnabled, chatLocalEvents, chatDefaultName, + compassEnabled, compassWidth, compassPrecision + ); + + return TweaksConfig.config; + } +} \ No newline at end of file diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java index ffe2080..7573dda 100644 --- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java +++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java @@ -2,12 +2,12 @@ package eu.m724.tweaks; import eu.m724.tweaks.chat.ChatCommands; import eu.m724.tweaks.chat.ChatManager; -import eu.m724.tweaks.door.DoorListener; +import eu.m724.tweaks.compass.CompassManager; +import eu.m724.tweaks.door.DoorManager; import eu.m724.tweaks.motd.MotdListener; import eu.m724.tweaks.ping.F3NameListener; import eu.m724.tweaks.ping.PingChecker; import eu.m724.tweaks.ping.PingCommands; -import eu.m724.tweaks.player.MusicPlayer; import eu.m724.tweaks.worldborder.WorldBorderManager; import org.bukkit.plugin.java.JavaPlugin; @@ -17,33 +17,48 @@ import java.util.Objects; public class TweaksPlugin extends JavaPlugin { @Override public void onEnable() { - ChatManager chatManager = new ChatManager(this); - chatManager.init(); + TweaksConfig config = TweaksConfig.load(this); - ChatCommands chatCommands = new ChatCommands(chatManager); - Objects.requireNonNull(getCommand("chat")).setExecutor(chatCommands); - Objects.requireNonNull(getCommand("chatmanage")).setExecutor(chatCommands); + new CompassManager(this).init(); - getServer().getPluginManager().registerEvents(new DoorListener(), this); + if (config.chatEnabled()) { + ChatManager chatManager = new ChatManager(this); + chatManager.init(); + + ChatCommands chatCommands = new ChatCommands(chatManager); + Objects.requireNonNull(getCommand("chat")).setExecutor(chatCommands); + Objects.requireNonNull(getCommand("chatmanage")).setExecutor(chatCommands); + } + + if (config.doorEnabled()) { + new DoorManager().init(this); + } + + if (config.brandEnabled()) { + new F3NameListener(this).init(); + } - new F3NameListener(this).init(); new PingChecker(this).init(); Objects.requireNonNull(getCommand("ping")).setExecutor(new PingCommands()); Objects.requireNonNull(getCommand("dkick")).setExecutor(new PingCommands()); - if (getServer().getPluginManager().getPlugin("voicechat") != null) { + /*if (getServer().getPluginManager().getPlugin("voicechat") != null) { new MusicPlayer(this).init(); } else { getLogger().warning("To use voice extensions, install \"Simple Voice Chat\""); + }*/ + + if (config.motdEnabled()) { + try { + new MotdListener(this).init(); + } catch (IOException e) { + getLogger().severe("Failed to initialize MOTD extension"); + throw new RuntimeException(e); + } } - try { - new MotdListener("example").init(this); - } catch (IOException e) { - getLogger().severe("Failed to initialize MOTD extension"); - throw new RuntimeException(e); + if (config.worldborderHide()) { + new WorldBorderManager().init(this); } - - new WorldBorderManager().init(this); } } diff --git a/src/main/java/eu/m724/tweaks/chat/ChatListener.java b/src/main/java/eu/m724/tweaks/chat/ChatListener.java index a00ece5..b1edecf 100644 --- a/src/main/java/eu/m724/tweaks/chat/ChatListener.java +++ b/src/main/java/eu/m724/tweaks/chat/ChatListener.java @@ -1,20 +1,24 @@ package eu.m724.tweaks.chat; +import eu.m724.tweaks.TweaksConfig; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; public class ChatListener implements Listener { private final ChatManager chatManager; + private final boolean localEvents; public ChatListener(ChatManager chatManager) { this.chatManager = chatManager; + this.localEvents = TweaksConfig.getConfig().chatLocalEvents(); } @EventHandler @@ -27,16 +31,18 @@ public class ChatListener implements Listener { .create(); player.spigot().sendMessage(component); - chatRoom.broadcast( - new ComponentBuilder() - .append(ChatFormatUtils.chatRoomPrefixShort(chatRoom)) - .append(ChatFormatUtils.formatPlayer(player)) - .append(" has joined the server").color(ChatColor.GREEN) - .create() - ); + if (localEvents) { + chatRoom.broadcast( + new ComponentBuilder() + .append(ChatFormatUtils.chatRoomPrefixShort(chatRoom)) + .append(ChatFormatUtils.formatPlayer(player)) + .append(" has joined the server").color(ChatColor.GREEN) + .create() + ); - // remove Minecraft join message - event.setJoinMessage(null); + // remove Minecraft join message + event.setJoinMessage(null); + } } @EventHandler @@ -44,16 +50,36 @@ public class ChatListener implements Listener { Player player = event.getPlayer(); ChatRoom chatRoom = chatManager.removePlayer(player); - chatRoom.broadcast( - new ComponentBuilder() - .append(ChatFormatUtils.chatRoomPrefixShort(chatRoom)) - .append(ChatFormatUtils.formatPlayer(player)) - .append(" has left the server").color(ChatColor.RED) - .create() - ); + if (localEvents) { + chatRoom.broadcast( + new ComponentBuilder() + .append(ChatFormatUtils.chatRoomPrefixShort(chatRoom)) + .append(ChatFormatUtils.formatPlayer(player)) + .append(" has left the server").color(ChatColor.RED) + .create() + ); - // remove Minecraft quit message - event.setQuitMessage(null); + // remove Minecraft quit message + event.setQuitMessage(null); + } + } + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + if (localEvents) { + Player player = event.getEntity(); + ChatRoom chatRoom = chatManager.getPlayerChatRoom(player); + + chatRoom.broadcast( + new ComponentBuilder() + .append(ChatFormatUtils.chatRoomPrefixShort(chatRoom)) + .append(event.getDeathMessage()) + .create() + ); + + // remove Minecraft death message + event.setDeathMessage(null); + } } @EventHandler diff --git a/src/main/java/eu/m724/tweaks/chat/ChatManager.java b/src/main/java/eu/m724/tweaks/chat/ChatManager.java index 39c99ef..68ce79e 100644 --- a/src/main/java/eu/m724/tweaks/chat/ChatManager.java +++ b/src/main/java/eu/m724/tweaks/chat/ChatManager.java @@ -1,5 +1,6 @@ package eu.m724.tweaks.chat; +import eu.m724.tweaks.TweaksConfig; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.NamespacedKey; @@ -15,6 +16,7 @@ import java.util.Map; public class ChatManager { private final Plugin plugin; private final NamespacedKey chatRoomKey; + private final String defaultRoom; private final Map playerMap = new HashMap<>(); private final Map roomIdMap = new HashMap<>(); @@ -22,10 +24,11 @@ public class ChatManager { public ChatManager(Plugin plugin) { this.plugin = plugin; this.chatRoomKey = new NamespacedKey(plugin, "chatRoom"); + this.defaultRoom = TweaksConfig.getConfig().chatDefaultName(); } public void init() { - getById("global"); + getById(defaultRoom); plugin.getServer().getPluginManager().registerEvents(new ChatListener(this), plugin); } @@ -53,10 +56,15 @@ public class ChatManager { ChatRoom chatRoom = roomIdMap.get(id); if (chatRoom == null) { - chatRoom = ChatRoomLoader.load(plugin, id); + if (id.equals(defaultRoom)) { + return new ChatRoom(defaultRoom, null, null); + } else { + chatRoom = ChatRoomLoader.load(plugin, id); + } roomIdMap.put(id, chatRoom); } + return chatRoom; } @@ -104,9 +112,9 @@ public class ChatManager { if (chatRoom == null) { String id = player.getPersistentDataContainer().get(chatRoomKey, PersistentDataType.STRING); - if (id == null) id = "global"; + if (id == null) id = defaultRoom; chatRoom = getById(id); - if (chatRoom == null) chatRoom = getById("global"); + if (chatRoom == null) chatRoom = getById(defaultRoom); chatRoom.players.add(player); playerMap.put(player, chatRoom); @@ -157,7 +165,7 @@ public class ChatManager { roomIdMap.remove(chatRoom.id); ChatRoomLoader.getFile(plugin, chatRoom.id).delete(); chatRoom.players.forEach(player -> { - setPlayerChatRoom(getById("global"), player); + setPlayerChatRoom(getById(defaultRoom), player); }); } diff --git a/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java b/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java index fd2f702..427788f 100644 --- a/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java +++ b/src/main/java/eu/m724/tweaks/chat/ChatRoomLoader.java @@ -57,10 +57,6 @@ public class ChatRoomLoader { * @return the chat room or null if no such chat room */ static ChatRoom load(Plugin plugin, String id) { - if (id.equals("global")) { - return new ChatRoom("global", null, null); - } - File chatRoomFile = getFile(plugin, id); if (!chatRoomFile.exists()) return null; diff --git a/src/main/java/eu/m724/tweaks/compass/CompassListener.java b/src/main/java/eu/m724/tweaks/compass/CompassListener.java new file mode 100644 index 0000000..45e191b --- /dev/null +++ b/src/main/java/eu/m724/tweaks/compass/CompassListener.java @@ -0,0 +1,133 @@ +package eu.m724.tweaks.compass; + +import eu.m724.tweaks.TweaksConfig; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; +import org.bukkit.inventory.meta.CompassMeta; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +// TODO dimension check +public class CompassListener implements Listener { + private final int precision = TweaksConfig.getConfig().compassPrecision(); // degrees every point + private final int width = TweaksConfig.getConfig().compassWidth(); // points left to right + + private final Map points = Map.of( + 0, ChatColor.DARK_GRAY + "S", + 90, ChatColor.DARK_GRAY + "W", + 180, ChatColor.DARK_GRAY + "N", + 270, ChatColor.DARK_GRAY + "E" + ); + + @EventHandler + public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) { + if (event.getMainHandItem().getType() == Material.COMPASS || event.getOffHandItem().getType() == Material.COMPASS) { + event.getPlayer().resetTitle(); + event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent()); + } + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); + CompassPlayerPreferences preferences = CompassPlayerPreferences.get(player); + + boolean mainHand = event.getPlayer().getInventory().getItemInMainHand().getType() == Material.COMPASS; + boolean offHand = event.getPlayer().getInventory().getItemInOffHand().getType() == Material.COMPASS; + boolean holding = mainHand || offHand; + + if (holding) { + preferences.visible = true; + } else { + if (preferences.visible) { + preferences.visible = false; + event.getPlayer().resetTitle(); + event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, new TextComponent()); + } + return; + } + + double yaw = wrapMod(event.getTo().getYaw(), 360.0); + + String[] visiblePoints = new String[width]; + Arrays.fill(visiblePoints, ChatColor.DARK_GRAY + "-"); + + Map currentPoints = new LinkedHashMap<>(points); + currentPoints.putAll(preferences.customPoints); + + // + + // TODO optimzie, caluclate that when needed + if (player.getBedSpawnLocation() != null) { + if (player.getBedSpawnLocation().getWorld().equals(player.getWorld())) { + double respawnYaw = calculateYaw(player.getLocation(), player.getBedSpawnLocation()); + System.out.println(respawnYaw); + currentPoints.put((int) respawnYaw, ChatColor.GREEN + "r"); + } + } + + if (mainHand) { + CompassMeta meta = (CompassMeta) player.getInventory().getItemInMainHand().getItemMeta(); + if (meta.isLodestoneTracked()) { + double lodeYaw = calculateYaw(player.getLocation(), meta.getLodestone()); + currentPoints.put((int) lodeYaw, ChatColor.GRAY + "L"); + } + } + + if (offHand) { + CompassMeta meta = (CompassMeta) player.getInventory().getItemInOffHand().getItemMeta(); + if (meta.isLodestoneTracked()) { + double lodeYaw = calculateYaw(player.getLocation(), meta.getLodestone()); + currentPoints.put((int) lodeYaw, ChatColor.GRAY + "L"); + } + } + + // + + for (Map.Entry entry : currentPoints.entrySet()) { + double distance = -wrapModRange(yaw - entry.getKey(), -180, 180); + int index = (int) (distance / precision + width / 2.0); + + if (index >= 0 && index < width) { + visiblePoints[index] = entry.getValue(); + } + } + + ComponentBuilder builder = new ComponentBuilder(); + for (String point : visiblePoints) { + builder.append(point + ' '); + } + + if (mainHand) { + event.getPlayer().sendTitle("", builder.build().toLegacyText(), 0, 70, 20); + } else { + event.getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, builder.create()); + } + } + + private double wrapModRange(double value, double start, double stop) { + double range = stop - start; + double result = start + (value - start - Math.floor((value - start) / range) * range); + return result == stop ? start : result; + } + + private double wrapMod(double value, double stop) { + double result = (value - Math.floor(value / stop) * stop); + return result == stop ? 0 : result; + } + + private double calculateYaw(Location source, Location target) { + return wrapMod(Math.atan2(source.getX() - target.getX(), target.getZ() - source.getZ()) * (180 / Math.PI), 360); + } +} diff --git a/src/main/java/eu/m724/tweaks/compass/CompassManager.java b/src/main/java/eu/m724/tweaks/compass/CompassManager.java new file mode 100644 index 0000000..4a8d2ae --- /dev/null +++ b/src/main/java/eu/m724/tweaks/compass/CompassManager.java @@ -0,0 +1,15 @@ +package eu.m724.tweaks.compass; + +import org.bukkit.plugin.Plugin; + +public class CompassManager { + private final Plugin plugin; + + public CompassManager(Plugin plugin) { + this.plugin = plugin; + } + + public void init() { + plugin.getServer().getPluginManager().registerEvents(new CompassListener(), plugin); + } +} diff --git a/src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java b/src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java new file mode 100644 index 0000000..b325a98 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/compass/CompassPlayerPreferences.java @@ -0,0 +1,17 @@ +package eu.m724.tweaks.compass; + +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class CompassPlayerPreferences { + static Map playerMap = new HashMap<>(); + + boolean visible = false; + Map customPoints = new HashMap<>(); + + static CompassPlayerPreferences get(Player player) { + return CompassPlayerPreferences.playerMap.computeIfAbsent(player, (k) -> new CompassPlayerPreferences()); + } +} diff --git a/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java b/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java new file mode 100644 index 0000000..063ef9f --- /dev/null +++ b/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java @@ -0,0 +1,57 @@ +package eu.m724.tweaks.door; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.type.Door; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockDamageAbortEvent; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.RayTraceResult; + +import java.util.concurrent.ThreadLocalRandom; + +public class DoorKnockListener implements Listener { + @EventHandler + public void onBlockDamageAbort(BlockDamageAbortEvent event) { + Block block = event.getBlock(); + if (!(block.getBlockData() instanceof Door)) return; + + World world = block.getLocation().getWorld(); + Player player = event.getPlayer(); + + // TODO maybe it would be faster to check just player and the door + RayTraceResult result = player.rayTraceBlocks(5); + if (result == null) return; + + Location hitLocation = result.getHitPosition().toLocation(world); + double distance = player.getEyeLocation().distanceSquared(hitLocation); + if (distance > 12) return; + + Sound sound = block.getType() == Material.IRON_DOOR ? Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR : Sound.ENTITY_ZOMBIE_ATTACK_WOODEN_DOOR; + float volume = player.isSneaking() ? 0.4f : 1f; + float pitch = player.getFallDistance() > 0 ? 1f : 1.5f; + + if (player.hasPotionEffect(PotionEffectType.NAUSEA)) { + pitch = ThreadLocalRandom.current().nextFloat(0.5f, 0.7f); + } + + PotionEffect fatigue = player.getPotionEffect(PotionEffectType.MINING_FATIGUE); + + if (fatigue != null) { + int level = fatigue.getAmplifier(); + volume /= level / 3f; + pitch /= level; + } + + volume *= (float) ((10.0 - Math.min(distance - 2, 10.0)) / 10.0); + + world.playSound(hitLocation, sound, volume, pitch); + //world.spawnParticle(Particle.BLOCK, hitLocation, (int) (10 * volume), door); + } +} diff --git a/src/main/java/eu/m724/tweaks/door/DoorManager.java b/src/main/java/eu/m724/tweaks/door/DoorManager.java new file mode 100644 index 0000000..4aa209a --- /dev/null +++ b/src/main/java/eu/m724/tweaks/door/DoorManager.java @@ -0,0 +1,18 @@ +package eu.m724.tweaks.door; + +import eu.m724.tweaks.TweaksConfig; +import org.bukkit.plugin.Plugin; + +public class DoorManager { + + public void init(Plugin plugin) { + if (TweaksConfig.getConfig().doorKnocking()) { + plugin.getServer().getPluginManager().registerEvents(new DoorKnockListener(), plugin); + } + + if (TweaksConfig.getConfig().doorDoubleOpen()) { + plugin.getServer().getPluginManager().registerEvents(new DoorOpenListener(), plugin); + } + } + +} diff --git a/src/main/java/eu/m724/tweaks/door/DoorListener.java b/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java similarity index 59% rename from src/main/java/eu/m724/tweaks/door/DoorListener.java rename to src/main/java/eu/m724/tweaks/door/DoorOpenListener.java index f2d948d..da9cdf7 100644 --- a/src/main/java/eu/m724/tweaks/door/DoorListener.java +++ b/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java @@ -4,56 +4,12 @@ import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.block.data.type.Door; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockDamageEvent; import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.util.RayTraceResult; -import java.util.concurrent.ThreadLocalRandom; - -public class DoorListener implements Listener { - @EventHandler - public void onBlockDamage(BlockDamageEvent event) { - Block block = event.getBlock(); - if (!(block.getBlockData() instanceof Door)) return; - - World world = block.getLocation().getWorld(); - Player player = event.getPlayer(); - - // TODO maybe it would be faster to check just player and the door - RayTraceResult result = player.rayTraceBlocks(5); - if (result == null) return; - - Location hitLocation = result.getHitPosition().toLocation(world); - double distance = player.getEyeLocation().distanceSquared(hitLocation); - if (distance > 12) return; - - Sound sound = block.getType() == Material.IRON_DOOR ? Sound.ENTITY_ZOMBIE_ATTACK_IRON_DOOR : Sound.ENTITY_ZOMBIE_ATTACK_WOODEN_DOOR; - float volume = player.isSneaking() ? 0.4f : 1f; - float pitch = player.getFallDistance() > 0 ? 1f : 1.5f; - - if (player.hasPotionEffect(PotionEffectType.NAUSEA)) { - pitch = ThreadLocalRandom.current().nextFloat(0.5f, 0.7f); - } - - PotionEffect fatigue = player.getPotionEffect(PotionEffectType.MINING_FATIGUE); - - if (fatigue != null) { - int level = fatigue.getAmplifier(); - volume /= level / 3f; - pitch /= level; - } - - volume *= (float) ((10.0 - Math.min(distance - 2, 10.0)) / 10.0); - - world.playSound(hitLocation, sound, volume, pitch); - //world.spawnParticle(Particle.BLOCK, hitLocation, (int) (10 * volume), door); - } +public class DoorOpenListener implements Listener { @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { @@ -109,4 +65,5 @@ public class DoorListener implements Listener { location.getBlock().setBlockData(nextDoor); } } + } diff --git a/src/main/java/eu/m724/tweaks/motd/MotdListener.java b/src/main/java/eu/m724/tweaks/motd/MotdListener.java index 40f879d..89afd37 100644 --- a/src/main/java/eu/m724/tweaks/motd/MotdListener.java +++ b/src/main/java/eu/m724/tweaks/motd/MotdListener.java @@ -5,6 +5,7 @@ import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.events.*; import com.comphenix.protocol.reflect.StructureModifier; import com.google.gson.JsonElement; +import eu.m724.tweaks.TweaksConfig; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.chat.ComponentSerializer; import net.minecraft.SharedConstants; @@ -21,15 +22,18 @@ import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; public class MotdListener { - private final String motdSetName; + private final Plugin plugin; private Component[] motds; - public MotdListener(String motdSetName) { - this.motdSetName = motdSetName; + public MotdListener(Plugin plugin) { + this.plugin = plugin; } - public void init(Plugin plugin) throws IOException { + public void init() throws IOException { + // TODO adding more MOTD features would require checking whether to enable set + + String motdSetName = TweaksConfig.getConfig().motdSet(); File motdSetsFile = new File(plugin.getDataFolder() + "/motd sets/" + motdSetName + ".txt"); // if the directory didn't exist create example motd sets @@ -46,8 +50,6 @@ public class MotdListener { }) .toArray(Component[]::new); - plugin.getLogger().info("Loaded %d MOTDs".formatted(motds.length)); - registerListener(plugin); } diff --git a/src/main/java/eu/m724/tweaks/ping/F3NameListener.java b/src/main/java/eu/m724/tweaks/ping/F3NameListener.java index 8cd17f6..7a638e4 100644 --- a/src/main/java/eu/m724/tweaks/ping/F3NameListener.java +++ b/src/main/java/eu/m724/tweaks/ping/F3NameListener.java @@ -4,6 +4,7 @@ import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.events.*; import com.comphenix.protocol.reflect.StructureModifier; +import eu.m724.tweaks.TweaksConfig; import net.minecraft.network.protocol.common.custom.BrandPayload; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; @@ -12,34 +13,35 @@ import org.bukkit.scheduler.BukkitRunnable; public class F3NameListener { private final Plugin plugin; + private boolean showPing, showMspt; + private String text; + public F3NameListener(Plugin plugin) { this.plugin = plugin; } public void init() { + showPing = TweaksConfig.getConfig().brandShowPing(); + showMspt = TweaksConfig.getConfig().brandShowMspt(); + text = TweaksConfig.getConfig().brandText(); + new BukkitRunnable() { @Override public void run() { - double mspt = PlayerPing.getMillisPerTick(); - boolean showMspt = mspt > 50.05; // mspt is at least like 50.01 because of some measuring overheads (I think) - for (Player player : plugin.getServer().getOnlinePlayers()) { - double ping = PlayerPing.getPingMillis(player); - String brand; - if (ping == 0) { - brand = "wait"; - } else { - if (showMspt) { - brand = "%.2f mspt | Ping: %.2f ms".formatted(mspt, PlayerPing.getPingMillis(player)); - } else { - brand = "Ping: %.2f ms".formatted(PlayerPing.getPingMillis(player)); - } + if (showPing) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + String text = getBrandText(player); + changeBrandFor(player, text); } - changeBrand(player, brand); + } else { + String text = getBrandText(null); + changeBrandAll(text); } } }.runTaskTimerAsynchronously(plugin, 0, 200); // 10 sec // this is just to make the server not send a brand on login, it doesn't ever run after login + // TODO is there much difference if we create a packet instead of modifying? ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( plugin, ListenerPriority.NORMAL, @@ -50,7 +52,7 @@ public class F3NameListener { PacketContainer packet = event.getPacket(); InternalStructure structure = new InternalStructure( - new BrandPayload("wait"), + new BrandPayload(text), new StructureModifier<>(BrandPayload.class) ); @@ -59,7 +61,29 @@ public class F3NameListener { }); } - private void changeBrand(Player player, String brand) { + /** + * Composes a brand name + * @param player player or null if server wide + * @return the brand name + */ + private String getBrandText(Player player) { + String _text = text; + + if (showMspt) { + double mspt = PlayerPing.getMillisPerTick(); + if (mspt > 50.05) { // mspt is at least like 50.01 because of some measuring overheads (I think) + _text += " | %.2f mspt".formatted(mspt); + } + } + + if (showPing) { + _text += " | %.2f ms".formatted(PlayerPing.getPingMillis(player)); + } + + return _text; + } + + private PacketContainer composeBrandPacket(String brand) { PacketContainer packet = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD); InternalStructure structure = new InternalStructure( @@ -68,6 +92,21 @@ public class F3NameListener { ); packet.getStructures().write(0, structure); - ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet); + + return packet; + } + + /** + * Changes server brand for all players on the server + */ + private void changeBrandAll(String brand) { + ProtocolLibrary.getProtocolManager().broadcastServerPacket(composeBrandPacket(brand)); + } + + /** + * Changes server brand for a player + */ + private void changeBrandFor(Player player, String brand) { + ProtocolLibrary.getProtocolManager().sendServerPacket(player, composeBrandPacket(brand)); } } diff --git a/src/main/java/eu/m724/tweaks/ping/PingChecker.java b/src/main/java/eu/m724/tweaks/ping/PingChecker.java index 19002a5..68690ba 100644 --- a/src/main/java/eu/m724/tweaks/ping/PingChecker.java +++ b/src/main/java/eu/m724/tweaks/ping/PingChecker.java @@ -1,68 +1,24 @@ package eu.m724.tweaks.ping; -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 com.comphenix.protocol.wrappers.MinecraftKey; -import org.bukkit.entity.Player; +import eu.m724.tweaks.TweaksConfig; import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitRunnable; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class PingChecker extends BukkitRunnable { +public class PingChecker { private final Plugin plugin; - // this is in nanoseconds - private final Map pending = new ConcurrentHashMap<>(); - public PingChecker(Plugin plugin) { this.plugin = plugin; } public void init() { - ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( - plugin, - ListenerPriority.NORMAL, - PacketType.Play.Client.COOKIE_RESPONSE - ) { - @Override - public void onPacketReceiving(PacketEvent event) { - // below line checks, whether the first (and sole) identifier field in the packet is minecraft:chk_ping - if (event.getPacket().getMinecraftKeys().read(0).getKey().equals("chk_ping")) { - Player player = event.getPlayer(); - long start = pending.remove(player); - PlayerPing.pings.put(player, System.nanoTime() - start); + if (TweaksConfig.getConfig().isProtocolLib()) { + plugin.getLogger().info("Using ProtocolLib for checking ping"); + new ProtocolPingChecker(plugin).start(); + } else { + plugin.getLogger().info("Using Spigot for checking ping"); + } - // gotta cancel because the server will kick - if (!PlayerPing.kickQueue.contains(player)) { - event.setCancelled(true); - } else { - PlayerPing.kickQueue.remove(player); - } - } - } - }); - - this.runTaskTimerAsynchronously(plugin, 0, 200); // 10 secs new MsptChecker().init(plugin); // TODO should this be here } - - @Override - public void run() { - plugin.getServer().getOnlinePlayers().forEach(player -> { - // dropped packets happen so timing out a request after 30 seconds - if (System.nanoTime() - pending.getOrDefault(player, 0L) < 30000000000L) return; - pending.put(player, System.nanoTime()); // here or at the bottom? probably doesn't matter - - PacketContainer packet = new PacketContainer(PacketType.Play.Server.COOKIE_REQUEST); - packet.getMinecraftKeys().write(0, new MinecraftKey("chk_ping")); - ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet); - }); - } } diff --git a/src/main/java/eu/m724/tweaks/ping/PingCommands.java b/src/main/java/eu/m724/tweaks/ping/PingCommands.java index de80367..e7a0488 100644 --- a/src/main/java/eu/m724/tweaks/ping/PingCommands.java +++ b/src/main/java/eu/m724/tweaks/ping/PingCommands.java @@ -1,5 +1,6 @@ package eu.m724.tweaks.ping; +import eu.m724.tweaks.TweaksConfig; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; @@ -20,6 +21,11 @@ public class PingCommands implements CommandExecutor { .create(); player.spigot().sendMessage(component); } else if (command.getName().equals("dkick")) { + if (!TweaksConfig.getConfig().isProtocolLib()) { + sender.sendMessage("This feature is not available"); + return true; + } + if (args.length == 0) { sender.sendMessage("Include one or more player names"); } else { diff --git a/src/main/java/eu/m724/tweaks/ping/PlayerPing.java b/src/main/java/eu/m724/tweaks/ping/PlayerPing.java index 2e9e59e..954fa8e 100644 --- a/src/main/java/eu/m724/tweaks/ping/PlayerPing.java +++ b/src/main/java/eu/m724/tweaks/ping/PlayerPing.java @@ -1,5 +1,6 @@ package eu.m724.tweaks.ping; +import eu.m724.tweaks.TweaksConfig; import org.bukkit.entity.Player; import java.util.Map; @@ -7,15 +8,23 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class PlayerPing { - // in nanos - static final Map pings = new ConcurrentHashMap<>(); - // nanos per tick - static volatile long nspt = 0L; + // if protocol lib is installed we use our own checker + private static final boolean external = TweaksConfig.getConfig().isProtocolLib(); + // TODO remove this feature because why static final Set kickQueue = ConcurrentHashMap.newKeySet(); + // current player latencies, in nanos + static final Map pings = new ConcurrentHashMap<>(); + // nanoseconds per tick + static volatile long nspt = 0L; + public static long getPingNanos(Player player) { - return pings.getOrDefault(player, -1L); + if (!external) { + return (long) player.getPing() * 1000000; + } else { + return pings.getOrDefault(player, -1L); + } } public static double getPingMillis(Player player) { diff --git a/src/main/java/eu/m724/tweaks/ping/ProtocolPingChecker.java b/src/main/java/eu/m724/tweaks/ping/ProtocolPingChecker.java new file mode 100644 index 0000000..96825e7 --- /dev/null +++ b/src/main/java/eu/m724/tweaks/ping/ProtocolPingChecker.java @@ -0,0 +1,66 @@ +package eu.m724.tweaks.ping; + +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 com.comphenix.protocol.wrappers.MinecraftKey; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ProtocolPingChecker extends BukkitRunnable { + // this is in nanoseconds + private final Map pending = new ConcurrentHashMap<>(); + private final Plugin plugin; + + ProtocolPingChecker(Plugin plugin) { + this.plugin = plugin; + } + + public void start() { + ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( + plugin, + ListenerPriority.NORMAL, + PacketType.Play.Client.COOKIE_RESPONSE + ) { + @Override + public void onPacketReceiving(PacketEvent event) { + // below line checks, whether the first (and sole) identifier field in the packet is minecraft:chk_ping + if (event.getPacket().getMinecraftKeys().read(0).getKey().equals("chk_ping")) { + Player player = event.getPlayer(); + + long start = pending.remove(player); + PlayerPing.pings.put(player, System.nanoTime() - start); + + // gotta cancel because the server will kick + if (!PlayerPing.kickQueue.contains(player)) { + event.setCancelled(true); + } else { + PlayerPing.kickQueue.remove(player); + } + } + } + }); + + this.runTaskTimerAsynchronously(plugin, 0, 200); // 10 secs + } + + @Override + public void run() { + plugin.getServer().getOnlinePlayers().forEach(player -> { + // dropped packets happen so timing out a request after 30 seconds + if (System.nanoTime() - pending.getOrDefault(player, 0L) < 30000000000L) return; + pending.put(player, System.nanoTime()); // here or at the bottom? probably doesn't matter + + PacketContainer packet = new PacketContainer(PacketType.Play.Server.COOKIE_REQUEST); + packet.getMinecraftKeys().write(0, new MinecraftKey("chk_ping")); + ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet); + }); + } +} diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderInfoPayload.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderInfoPayload.java new file mode 100644 index 0000000..ed0055d --- /dev/null +++ b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderInfoPayload.java @@ -0,0 +1,28 @@ +package eu.m724.tweaks.worldborder; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +// TODO I could do this with API but yeah I'm more comfortable with this + +public record WorldBorderInfoPayload(int extensionRadius) implements CustomPacketPayload { + private static final Type TYPE = + new Type<>(ResourceLocation.tryBuild("tweaks724", "worldborder")); + + public static final StreamCodec STREAM_CODEC = + CustomPacketPayload.codec(WorldBorderInfoPayload::write, WorldBorderInfoPayload::new); + + public Type type() { + return TYPE; + } + + private WorldBorderInfoPayload(FriendlyByteBuf byteBuf) { + this(byteBuf.readVarInt()); + } + + private void write(FriendlyByteBuf byteBuf) { + byteBuf.writeVarInt(extensionRadius); + } +} diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java index 4e20789..49e7153 100644 --- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java +++ b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderManager.java @@ -2,19 +2,21 @@ package eu.m724.tweaks.worldborder; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.events.*; -import com.comphenix.protocol.reflect.StructureModifier; -import net.minecraft.SharedConstants; -import net.minecraft.network.chat.Component; -import net.minecraft.network.protocol.game.ClientboundInitializeBorderPacket; -import net.minecraft.network.protocol.status.ServerStatus; +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 org.bukkit.plugin.Plugin; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; +import java.nio.ByteBuffer; public class WorldBorderManager { + private static final int EXTENSION_RADIUS = 512; + public void init(Plugin plugin) { + plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "tweaks724:worldborder"); + byte[] infoArray = ByteBuffer.allocate(4).putInt(EXTENSION_RADIUS).array(); + ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( plugin, ListenerPriority.NORMAL, @@ -23,12 +25,43 @@ public class WorldBorderManager { @Override public void onPacketSending(PacketEvent event) { PacketContainer packet = event.getPacket(); - event.getPacket().getDoubles().write(2, 60000000.0); - event.getPacket().getDoubles().write(3, 60000000.0); + // old diameter + packet.getDoubles().write(2, packet.getDoubles().read(2) + EXTENSION_RADIUS * 2); + // new diameter + packet.getDoubles().write(3, packet.getDoubles().read(3) + EXTENSION_RADIUS * 2); - System.out.println( - packet - ); + // border radius + // packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS); + // warning distance + packet.getIntegers().write(1, packet.getIntegers().read(1) + EXTENSION_RADIUS); + + event.getPlayer().sendPluginMessage(plugin, "tweaks724:worldborder", infoArray); + } + }); + + ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( + plugin, + ListenerPriority.NORMAL, + PacketType.Play.Server.SET_BORDER_SIZE + ) { + @Override + public void onPacketSending(PacketEvent event) { + PacketContainer packet = event.getPacket(); + // diameter + packet.getDoubles().write(0, packet.getDoubles().read(0) + EXTENSION_RADIUS * 2); + } + }); + + ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter( + plugin, + ListenerPriority.NORMAL, + PacketType.Play.Server.SET_BORDER_WARNING_DISTANCE + ) { + @Override + public void onPacketSending(PacketEvent event) { + PacketContainer packet = event.getPacket(); + // warning distance + packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS); } }); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a23b980 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,50 @@ +worldborder: + # Hides the world border at 30 mil + # Client side so it won't make the world bigger or anything + # Warning: Don't use /worldborder while this is on + hide: true + +# "Server brand" (second line of F3) +brand: + enabled: true + # Custom text, "" to not use + text: "My server" + # Show a player's ping + showPing: true + # Show mspt (millis per tick) if server is lagging + showMspt: true + +doors: + # Opening one door also opens the adjacent one + doubleOpen: true + # Make a sound on left-clicking + knocking: true + +motd: + # Name of the set containing the MOTDs + # (random displayed every ping) + # "" or false to disable + set: "example" + +chat: + # Enable chatrooms + enabled: true + # Whether event messages like join, quit, death (I think that's all needed) + # are only within the player's chatroom + localEvents: true + # Name of the default / global chatroom + defaultName: "global" + +compass: + # Compass will be shown in a text form + enabled: true + # How much points (each point is separated with a space) + # I suggest making this uneven to make center... in center + width: 9 + # How many degrees every point + precision: 10 + +# Finally, thank you for downloading Tweaks724, I hope you enjoy! + +# Don't modify unless told to +magic number dont modify this: 1 \ No newline at end of file