All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cryptomorin.xseries.messages.ActionBar Maven / Gradle / Ivy

There is a newer version: 12.1.0
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2024 Crypto Morin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.cryptomorin.xseries.messages;

import com.cryptomorin.xseries.reflection.XReflection;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftConnection;
import com.cryptomorin.xseries.reflection.minecraft.MinecraftPackage;
import com.google.common.base.Strings;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Objects;
import java.util.concurrent.Callable;

import static com.cryptomorin.xseries.reflection.XReflection.*;

/**
 * A reflection API for action bars in Minecraft.
 * Fully optimized - Supports 1.8.8+ and above.
 * Requires ReflectionUtils.
 * Messages are not colorized by default.
 * 

* Action bars are text messages that appear above * the player's hotbar * Note that this is different from the text appeared when switching between items. * Those messages show the item's name and are different from action bars. * The only natural way of displaying action bars is when mounting. *

* Action bars cannot fade or stay like titles. * For static Action bars you'll need to send the packet every * 2 seconds (40 ticks) for it to stay on the screen without fading. *

* PacketPlayOutTitle * * @author Crypto Morin * @version 4.0.0 * @see XReflection */ public final class ActionBar { /** * If the server is running Spigot which has an official ActionBar API. * This should technically be available from 1.9, but TextComponent API * has some issues regarding colors from 1.9-1.11, so we're still going * to use NMS for anything below 1.12 * We're not going to support Bukkit. */ private static final boolean USE_SPIGOT_API = XReflection.supports(12); /** * ChatComponentText JSON message builder. */ private static final MethodHandle CHAT_COMPONENT_TEXT; /** * PacketPlayOutChat */ private static final MethodHandle PACKET_PLAY_OUT_CHAT; /** * GAME_INFO enum constant. */ private static final Object CHAT_MESSAGE_TYPE; private static final char TIME_SPECIFIER_START = '^', TIME_SPECIFIER_END = '|'; static { MethodHandle packet = null; MethodHandle chatComp = null; Object chatMsgType = null; if (!USE_SPIGOT_API) { // Supporting 1.12+ is not necessary, the package guards are just for readability. // Since these methods are old, we don't need to add Mojang mappings MethodHandles.Lookup lookup = MethodHandles.lookup(); Class packetPlayOutChatClass = ofMinecraft().inPackage(MinecraftPackage.NMS, "network.protocol.game") .named("PacketPlayOutChat").unreflect(); Class iChatBaseComponentClass = ofMinecraft().inPackage(MinecraftPackage.NMS, "network.chat") .named("IChatBaseComponent").unreflect(); Class ChatSerializerClass = ofMinecraft().inPackage(MinecraftPackage.NMS, "network.chat") .named("IChatBaseComponent$ChatSerializer").unreflect(); try { // JSON Message Builder // network.chat.ChatComponentText is for raw messages, we need to support colors. chatComp = lookup.findStatic(ChatSerializerClass, "a", MethodType.methodType(iChatBaseComponentClass, String.class)); // Game Info Message Type Class chatMessageTypeClass = Class.forName( NMS_PACKAGE + v(17, "network.chat").orElse("") + "ChatMessageType" ); // Packet Constructor MethodType type = MethodType.methodType(void.class, iChatBaseComponentClass, chatMessageTypeClass); for (Object obj : chatMessageTypeClass.getEnumConstants()) { String name = obj.toString(); if (name.equals("GAME_INFO") || name.equalsIgnoreCase("ACTION_BAR")) { chatMsgType = obj; break; } } packet = lookup.findConstructor(packetPlayOutChatClass, type); } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException ignored) { try { // Game Info Message Type chatMsgType = (byte) 2; // Packet Constructor packet = lookup.findConstructor(packetPlayOutChatClass, MethodType.methodType(void.class, iChatBaseComponentClass, byte.class)); } catch (NoSuchMethodException | IllegalAccessException ex) { ex.printStackTrace(); } } } CHAT_MESSAGE_TYPE = chatMsgType; CHAT_COMPONENT_TEXT = chatComp; PACKET_PLAY_OUT_CHAT = packet; } private ActionBar() { } /** * Sends an action bar to a player. * This particular method supports a special prefix for * configuring the time of the actionbar. *

* Format: {@code ^number|} *
* where {@code number} is in seconds. *
* E.g. {@code ^7|&2Hello &4World!} * will keep the actionbar active for 7 seconds. * * @param player the player to send the action bar to. * @param message the message to send. * @see #sendActionBar(Plugin, Player, String, long) * @since 3.2.0 */ public static void sendActionBar(@Nonnull Plugin plugin, @Nonnull Player player, @Nullable String message) { if (!Strings.isNullOrEmpty(message)) { if (message.charAt(0) == TIME_SPECIFIER_START) { int end = message.indexOf(TIME_SPECIFIER_END); if (end != -1) { int time = 0; try { time = Integer.parseInt(message.substring(1, end)) * 20; } catch (NumberFormatException ignored) { } if (time >= 0) sendActionBar(plugin, player, message.substring(end + 1), time); } } } sendActionBar(player, message); } /** * Sends an action bar to a player. * * @param player the player to send the action bar to. * @param message the message to send. * @see #sendActionBar(Plugin, Player, String, long) * @since 1.0.0 */ @SuppressWarnings("DynamicRegexReplaceableByCompiledPattern") public static void sendActionBar(@Nonnull Player player, @Nullable String message) { Objects.requireNonNull(player, "Cannot send action bar to null player"); Objects.requireNonNull(message, "Cannot send null actionbar message"); if (USE_SPIGOT_API) { // noinspection deprecation player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); return; } try { // We need to escape both \ and " to avoid all possiblities of breaking JSON syntax and causing an exception. Object component = CHAT_COMPONENT_TEXT.invoke("{\"text\":\"" + message.replace("\\", "\\\\").replace("\"", "\\\"") + "\"}"); Object packet = PACKET_PLAY_OUT_CHAT.invoke(component, CHAT_MESSAGE_TYPE); MinecraftConnection.sendPacket(player, packet); } catch (Throwable throwable) { throwable.printStackTrace(); } } /** * Sends an action bar to a player for a specific amount of ticks. * * @param plugin the plugin handling the message scheduler. * @param player the player to send the action bar to. * @param message the message to send. * @param duration the duration to keep the action bar in ticks. * @see #sendActionBarWhile(Plugin, Player, String, Callable) * @since 1.0.0 */ public static void sendActionBar(@Nonnull Plugin plugin, @Nonnull Player player, @Nullable String message, long duration) { if (duration < 1) return; Objects.requireNonNull(plugin, "Cannot send consistent actionbar with null plugin"); Objects.requireNonNull(player, "Cannot send actionbar to null player"); Objects.requireNonNull(message, "Cannot send null actionbar message"); new BukkitRunnable() { long repeater = duration; @Override public void run() { sendActionBar(player, message); repeater -= 40L; if (repeater - 40L < -20L) cancel(); } }.runTaskTimerAsynchronously(plugin, 0L, 40L); } /** * Sends an action bar all the online players. * * @param message the message to send. * @see #sendActionBar(Player, String) * @since 1.0.0 */ public static void sendPlayersActionBar(@Nullable String message) { for (Player player : Bukkit.getOnlinePlayers()) sendActionBar(player, message); } /** * Clear the action bar by sending an empty message. * * @param player the player to send the action bar to. * @see #sendActionBar(Player, String) * @since 2.1.1 */ public static void clearActionBar(@Nonnull Player player) { sendActionBar(player, " "); } /** * Clear the action bar by sending an empty message to all the online players. * * @see #clearActionBar(Player) * @since 2.1.1 */ public static void clearPlayersActionBar() { for (Player player : Bukkit.getOnlinePlayers()) clearActionBar(player); } /** * Sends an action bar to a player for a specific amount of ticks. * Plugin instance should be changed in this method for the schedulers. *

* If the caller returns true, the action bar will continue. * If the caller returns false, action bar will not be sent anymore. * * @param plugin the plugin handling the message scheduler. * @param player the player to send the action bar to. * @param message the message to send. The message will not be updated. * @param callable the condition for the action bar to continue. * @see #sendActionBar(Plugin, Player, String, long) * @since 1.0.0 */ public static void sendActionBarWhile(@Nonnull Plugin plugin, @Nonnull Player player, @Nullable String message, @Nonnull Callable callable) { new BukkitRunnable() { @Override public void run() { try { if (!callable.call()) { cancel(); return; } } catch (Exception ex) { ex.printStackTrace(); } sendActionBar(player, message); } // Re-sends the messages every 2 seconds, so it doesn't go away from the player's screen. }.runTaskTimerAsynchronously(plugin, 0L, 40L); } /** * Sends an action bar to a player for a specific amount of ticks. *

* If the caller returns true, the action bar will continue. * If the caller returns false, action bar will not be sent anymore. * * @param plugin the plugin handling the message scheduler. * @param player the player to send the action bar to. * @param message the message to send. The message will be updated. * @param callable the condition for the action bar to continue. * @see #sendActionBarWhile(Plugin, Player, String, Callable) * @since 1.0.0 */ public static void sendActionBarWhile(@Nonnull Plugin plugin, @Nonnull Player player, @Nullable Callable message, @Nonnull Callable callable) { new BukkitRunnable() { @Override public void run() { try { if (!callable.call()) { cancel(); return; } sendActionBar(player, message.call()); } catch (Exception ex) { ex.printStackTrace(); } } // Re-sends the messages every 2 seconds, so it doesn't go away from the player's screen. }.runTaskTimerAsynchronously(plugin, 0L, 40L); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy