Skip to content

Instantly share code, notes, and snippets.

@LOOHP
Last active April 22, 2023 23:40
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LOOHP/550f861ab4ad219cf74dc18c214ba530 to your computer and use it in GitHub Desktop.
Save LOOHP/550f861ab4ad219cf74dc18c214ba530 to your computer and use it in GitHub Desktop.
Source code of EditableSigns (https://www.spigotmc.org/resources/93850/)
package com.loohp.editablesigns;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Sign;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import net.md_5.bungee.api.ChatColor;
public class EditableSigns extends JavaPlugin implements Listener {
public static final Pattern REVERT_COLOR_CODE_PATTERN = Pattern.compile("\u00a7[0-9A-Fa-fk-or]|\u00a7x(?:\u00a7[0-9A-Fa-f]){6}");
private static final boolean FOLIA = classExist("io.papermc.paper.threadedregions.scheduler.AsyncScheduler");
public static EditableSigns plugin;
public static boolean handleLegacyColorCodes = true;
public static String reloadPluginMessage = "";
public static String noPermissionMessage = "";
private static boolean useBukkitOpenSign;
private static Constructor<?> nmsBlockPositionConstructor;
private static Constructor<?> nmsPacketPlayOutOpenSignEditorConstructor;
private static Class<?> craftWorldClass;
private static Method craftWorldGetHandleMethod;
private static boolean post118GetBlockEntity;
private static Method getTileEntityMethod;
private static Class<?> nmsTileEntitySignClass;
private static Class<?> nmsEntityHumanClass;
private static boolean tileEntitySignUsesModernMethods;
private static Field nmsTileEntitySignIsEditableField;
private static Field nmsTileEntitySignEntityHumanField;
private static Method nmsTileEntitySignSetEditableMethod;
private static Method nmsTileEntitySignSetUUIDMethod;
private static Class<?> craftPlayerClass;
private static Method craftPlayerGetHandleMethod;
private static Field playerConnectionField;
private static Class<?> nmsPacketClass;
private static Method sendPacketMethod;
private static boolean classExist(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
public static void runTaskLater(Plugin plugin, Location location, Runnable runnable, int delay) {
if (FOLIA) {
//noinspection rawtypes,unchecked
Bukkit.getRegionScheduler().runDelayed(plugin, location, (Consumer) st -> runnable.run(), delay);
} else {
Bukkit.getScheduler().runTaskLater(plugin, runnable, delay);
}
}
@Override
public void onEnable() {
plugin = this;
getConfig().options().copyDefaults(true);
saveConfig();
loadConfig();
try {
if (Arrays.stream(Player.class.getMethods()).noneMatch(each -> each.getName().equals("openSign"))) {
useBukkitOpenSign = false;
Class<?> nmsPacketPlayOutOpenSignEditorClass = getNMSClass("net.minecraft.server.%s.PacketPlayOutOpenSignEditor", "net.minecraft.network.protocol.game.PacketPlayOutOpenSignEditor");
Class<?> nmsBlockPositionClass = getNMSClass("net.minecraft.server.%s.BlockPosition", "net.minecraft.core.BlockPosition");
nmsBlockPositionConstructor = nmsBlockPositionClass.getConstructor(int.class, int.class, int.class);
nmsPacketPlayOutOpenSignEditorConstructor = nmsPacketPlayOutOpenSignEditorClass.getConstructor(nmsBlockPositionClass);
craftWorldClass = getNMSClass("org.bukkit.craftbukkit.%s.CraftWorld");
craftWorldGetHandleMethod = craftWorldClass.getMethod("getHandle");
post118GetBlockEntity = false;
try {
getTileEntityMethod = craftWorldGetHandleMethod.getReturnType().getMethod("getTileEntity", nmsBlockPositionClass);
} catch (Exception e) {
getTileEntityMethod = craftWorldGetHandleMethod.getReturnType().getMethod("getBlockEntity", nmsBlockPositionClass, boolean.class);
post118GetBlockEntity = true;
}
nmsTileEntitySignClass = getNMSClass("net.minecraft.server.%s.TileEntitySign", "net.minecraft.world.level.block.entity.TileEntitySign");
nmsEntityHumanClass = getNMSClass("net.minecraft.server.%s.EntityHuman", "net.minecraft.world.entity.player.EntityHuman");
tileEntitySignUsesModernMethods = false;
try {
nmsTileEntitySignIsEditableField = nmsTileEntitySignClass.getDeclaredField("isEditable");
nmsTileEntitySignEntityHumanField = Arrays.asList(nmsTileEntitySignClass.getDeclaredFields()).stream().filter(each -> each.getType().equals(nmsEntityHumanClass)).findFirst().get();
} catch (NoSuchFieldException e) {
nmsTileEntitySignSetEditableMethod = nmsTileEntitySignClass.getMethod("a", boolean.class);
nmsTileEntitySignSetUUIDMethod = nmsTileEntitySignClass.getMethod("a", UUID.class);
tileEntitySignUsesModernMethods = true;
}
craftPlayerClass = getNMSClass("org.bukkit.craftbukkit.%s.entity.CraftPlayer");
craftPlayerGetHandleMethod = craftPlayerClass.getMethod("getHandle");
try {
playerConnectionField = craftPlayerGetHandleMethod.getReturnType().getField("playerConnection");
} catch (NoSuchFieldException e) {
playerConnectionField = craftPlayerGetHandleMethod.getReturnType().getField("b");
}
nmsPacketClass = getNMSClass("net.minecraft.server.%s.Packet", "net.minecraft.network.protocol.Packet");
try {
sendPacketMethod = playerConnectionField.getType().getMethod("sendPacket", nmsPacketClass);
} catch (NoSuchMethodException e) {
sendPacketMethod = playerConnectionField.getType().getMethod("a", nmsPacketClass);
}
} else {
useBukkitOpenSign = true;
}
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
getCommand("editablesigns").setExecutor(this);
getServer().getPluginManager().registerEvents(this, this);
getServer().getConsoleSender().sendMessage(ChatColor.GREEN + "[EditableSigns] EditableSigns has been enabled.");
}
@Override
public void onDisable() {
getServer().getConsoleSender().sendMessage(ChatColor.RED + "[EditableSigns] EditableSigns has been disabled.");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(ChatColor.WHITE + "EditableSigns written by LOOHP!");
sender.sendMessage(ChatColor.GOLD + "You are running EditableSigns version: " + getDescription().getVersion());
return true;
}
if (args[0].equalsIgnoreCase("reload")) {
if (sender.hasPermission("editablesigns.reload")) {
reloadConfig();
sender.sendMessage(reloadPluginMessage);
} else {
sender.sendMessage(noPermissionMessage);
}
return true;
}
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', Bukkit.spigot().getConfig().getString("messages.unknown-command")));
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
List<String> tab = new ArrayList<>();
switch (args.length) {
case 0:
if (sender.hasPermission("editablesigns.reload")) {
tab.add("reload");
}
return tab;
case 1:
if (sender.hasPermission("editablesigns.reload")) {
if ("reload".startsWith(args[0].toLowerCase())) {
tab.add("reload");
}
}
return tab;
}
return tab;
}
public void loadConfig() {
reloadConfig();
handleLegacyColorCodes = getConfig().getBoolean("HandleLegacyColorCodes");
reloadPluginMessage = ChatColor.translateAlternateColorCodes('&', getConfig().getString("Messages.Reload"));
noPermissionMessage = ChatColor.translateAlternateColorCodes('&', getConfig().getString("Messages.NoPermission"));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
Player player = event.getPlayer();
if (!player.hasPermission("editablesigns.use")) {
return;
}
Block block = event.getClickedBlock();
if (block != null && block.getType().toString().contains("SIGN") && !player.isSneaking() && event.getAction().equals(Action.RIGHT_CLICK_BLOCK) && isInteractingWithAir(player)) {
Material material = block.getType();
Block attachedBlock;
BlockData data = block.getBlockData();
if (data instanceof Directional) {
Directional directional = (Directional) data;
attachedBlock = block.getRelative(directional.getFacing().getOppositeFace());
} else {
attachedBlock = block.getRelative(BlockFace.DOWN);
}
BlockPlaceEvent placeEvent = new BlockPlaceEvent(block, block.getState(), attachedBlock, new ItemStack(material), player, true, EquipmentSlot.HAND);
Bukkit.getPluginManager().callEvent(placeEvent);
if (!placeEvent.isCancelled()) {
Sign sign = (Sign) block.getState();
if (handleLegacyColorCodes) {
String[] lines = sign.getLines();
for (int i = 0; i < lines.length; i++) {
sign.setLine(i, revertColorCode('&', lines[i]));
}
sign.update();
}
runTaskLater(this, block.getLocation(), () -> {
if (useBukkitOpenSign) {
sign.setEditable(true);
sign.update();
runTaskLater(this, block.getLocation(), () -> player.openSign(sign), 1);
} else {
try {
Object entityPlayer = craftPlayerGetHandleMethod.invoke(craftPlayerClass.cast(player));
Object blockPosition = nmsBlockPositionConstructor.newInstance(block.getX(), block.getY(), block.getZ());
Object tileEntity;
if (post118GetBlockEntity) {
tileEntity = getTileEntityMethod.invoke(craftWorldGetHandleMethod.invoke(craftWorldClass.cast(block.getWorld())), blockPosition, true);
} else {
tileEntity = getTileEntityMethod.invoke(craftWorldGetHandleMethod.invoke(craftWorldClass.cast(block.getWorld())), blockPosition);
}
Object nmsSign = nmsTileEntitySignClass.cast(tileEntity);
if (tileEntitySignUsesModernMethods) {
nmsTileEntitySignSetEditableMethod.invoke(nmsSign, true);
nmsTileEntitySignSetUUIDMethod.invoke(nmsSign, player.getUniqueId());
} else {
nmsTileEntitySignIsEditableField.setAccessible(true);
nmsTileEntitySignIsEditableField.set(nmsSign, true);
nmsTileEntitySignEntityHumanField.setAccessible(true);
nmsTileEntitySignEntityHumanField.set(nmsSign, nmsEntityHumanClass.cast(entityPlayer));
}
Object packet = nmsPacketPlayOutOpenSignEditorConstructor.newInstance(blockPosition);
sendPacketMethod.invoke(playerConnectionField.get(entityPlayer), packet);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException |
InvocationTargetException e) {
e.printStackTrace();
}
}
}, 2);
}
}
}
@EventHandler(priority = EventPriority.NORMAL)
public void onSignChange(SignChangeEvent event) {
if (!handleLegacyColorCodes) {
return;
}
String[] lines = event.getLines();
for (int i = 0; i < lines.length; i++) {
event.setLine(i, ChatColor.translateAlternateColorCodes('&', lines[i]));
}
}
private String revertColorCode(char code, String str) {
Matcher matcher = REVERT_COLOR_CODE_PATTERN.matcher(str);
StringBuffer sb = new StringBuffer(str.length());
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group().replace('\u00a7', code));
}
matcher.appendTail(sb);
return sb.toString();
}
private boolean isInteractingWithAir(Player player) {
try {
ItemStack mainHand = player.getEquipment().getItemInMainHand();
ItemStack offHand = player.getEquipment().getItemInOffHand();
return (mainHand == null || mainHand.getType().equals(Material.AIR)) && (offHand == null || !offHand.getType().toString().contains("DYE"));
} catch (Throwable e) {
@SuppressWarnings("deprecation")
ItemStack item = player.getItemInHand();
return item == null || item.getType().equals(Material.AIR);
}
}
private static Class<?> getNMSClass(String path, String... paths) throws ClassNotFoundException {
String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
ClassNotFoundException error = null;
try {
return Class.forName(path.replace("%s", version));
} catch (ClassNotFoundException e) {
error = e;
}
for (String classpath : paths) {
try {
return Class.forName(classpath.replace("%s", version));
} catch (ClassNotFoundException e) {
error = e;
}
}
throw error;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment