├── .gitignore ├── NPCAPI.iml ├── README.md ├── pom.xml └── src └── main ├── java └── dev │ └── jcsoftware │ └── npcs │ ├── NMSHelper.java │ ├── NPC.java │ ├── NPCManager.java │ ├── NPCOptions.java │ ├── ServerVersion.java │ ├── events │ ├── NPCClickAction.java │ └── NPCInteractionEvent.java │ ├── example │ ├── NPCPlugin.java │ └── gui │ │ └── ServerMenuGUI.java │ ├── utility │ └── StringUtility.java │ └── versioned │ ├── NPC_Reflection.java │ └── NPC_v1_16_R3.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea -------------------------------------------------------------------------------- /NPCAPI.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NPCAPI 2 | 3 | I'll add more documentation on how the API works later. For now, just check out the example plugin located in main/dev/jcsoftware/npcs/example 4 | 5 | Learn how this works: https://www.youtube.com/watch?v=Avwg6ZCQX1o 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | dev.jcsoftware 8 | NPCS 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | NPCPlugin 13 | 14 | 15 | 1.8 16 | UTF-8 17 | 18 | 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-compiler-plugin 24 | 3.8.1 25 | 26 | ${java.version} 27 | ${java.version} 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-shade-plugin 33 | 3.2.4 34 | 35 | 36 | package 37 | 38 | shade 39 | 40 | 41 | false 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | src/main/resources 50 | true 51 | 52 | 53 | 54 | 55 | 56 | 57 | spigotmc-repo 58 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 59 | 60 | 61 | jitpack.io 62 | https://jitpack.io 63 | 64 | 65 | sonatype 66 | https://oss.sonatype.org/content/groups/public/ 67 | 68 | 69 | dmulloy2-repo 70 | https://repo.dmulloy2.net/nexus/repository/public/ 71 | 72 | 73 | 74 | 75 | 76 | org.spigotmc 77 | spigot 78 | 1.16.5-R0.1-SNAPSHOT 79 | provided 80 | 81 | 82 | com.github.JordanOsterberg 83 | MCGUIAPI 84 | 1.0.2 85 | 86 | 87 | org.projectlombok 88 | lombok 89 | 1.18.12 90 | 91 | 92 | com.comphenix.protocol 93 | ProtocolLib 94 | 4.5.0 95 | provided 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/NMSHelper.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs; 2 | 3 | import lombok.Getter; 4 | import lombok.SneakyThrows; 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.entity.Player; 7 | 8 | public class NMSHelper { 9 | @Getter private static final NMSHelper instance = new NMSHelper(); 10 | 11 | @Getter private final ServerVersion serverVersion; 12 | private final String serverVersionString; 13 | 14 | private NMSHelper() { 15 | serverVersionString = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; 16 | ServerVersion version = ServerVersion.UNKNOWN; 17 | for (ServerVersion option : ServerVersion.values()) { 18 | if (option.name().equalsIgnoreCase(serverVersionString)) { 19 | version = option; 20 | } 21 | } 22 | this.serverVersion = version; 23 | } 24 | 25 | @SneakyThrows 26 | public void sendPacket(Player player, Object packet) { 27 | if (player == null) return; 28 | Object handle = getHandle(player); 29 | Object playerConnection = handle.getClass().getField("playerConnection").get(handle); 30 | 31 | playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet")).invoke(playerConnection, packet); 32 | } 33 | 34 | @SneakyThrows 35 | public Object getHandle(Player player) { 36 | return player.getClass().getMethod("getHandle").invoke(player); 37 | } 38 | 39 | @SneakyThrows 40 | public Class getNMSClass(String name) { 41 | return Class.forName("net.minecraft.server." + getServerVersion() + "." + name); 42 | } 43 | 44 | @SneakyThrows 45 | public Class getCraftBukkitClass(String name) { 46 | return Class.forName("org.bukkit.craftbukkit." + getServerVersion() + "." + name); 47 | } 48 | 49 | public String getServerVersionString() { 50 | return serverVersionString; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/NPC.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.entity.Player; 5 | 6 | public interface NPC { 7 | String getName(); 8 | void showTo(Player player); 9 | void hideFrom(Player player); 10 | void delete(); 11 | Location getLocation(); 12 | int getId(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/NPCManager.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs; 2 | 3 | import com.comphenix.protocol.PacketType; 4 | import com.comphenix.protocol.ProtocolLibrary; 5 | import com.comphenix.protocol.events.PacketAdapter; 6 | import com.comphenix.protocol.events.PacketEvent; 7 | import com.comphenix.protocol.wrappers.EnumWrappers; 8 | import com.google.common.cache.Cache; 9 | import com.google.common.cache.CacheBuilder; 10 | import dev.jcsoftware.npcs.events.NPCClickAction; 11 | import dev.jcsoftware.npcs.events.NPCInteractionEvent; 12 | import dev.jcsoftware.npcs.versioned.NPC_Reflection; 13 | import dev.jcsoftware.npcs.versioned.NPC_v1_16_R3; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.entity.Player; 16 | import org.bukkit.plugin.java.JavaPlugin; 17 | 18 | import java.util.HashSet; 19 | import java.util.Optional; 20 | import java.util.Set; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | public class NPCManager { 24 | private final JavaPlugin plugin; 25 | private final boolean useReflection; 26 | 27 | private final Set registeredNPCs = new HashSet<>(); 28 | 29 | public NPCManager(JavaPlugin plugin, boolean useReflection) { 30 | this.plugin = plugin; 31 | this.useReflection = useReflection; 32 | 33 | ProtocolLibrary.getProtocolManager().addPacketListener( 34 | new PacketAdapter(plugin, PacketType.Play.Client.USE_ENTITY) { 35 | @Override 36 | public void onPacketReceiving(PacketEvent event) { 37 | EnumWrappers.EntityUseAction useAction = event.getPacket().getEntityUseActions().read(0); 38 | int entityId = event.getPacket().getIntegers().read(0); 39 | handleEntityClick(event.getPlayer(), entityId, NPCClickAction.fromProtocolLibAction(useAction)); 40 | } 41 | } 42 | ); 43 | } 44 | 45 | private final Cache clickedNPCCache = CacheBuilder.newBuilder() 46 | .expireAfterWrite(1L, TimeUnit.SECONDS) 47 | .build(); 48 | 49 | private void handleEntityClick(Player player, int entityId, NPCClickAction action) { 50 | registeredNPCs.stream() 51 | .filter(npc -> npc.getId() == entityId) 52 | .forEach(npc -> Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> { 53 | NPC previouslyClickedNPC = clickedNPCCache.getIfPresent(player); 54 | if (previouslyClickedNPC != null && previouslyClickedNPC.equals(npc)) return; // If they've clicked this same NPC in the last 0.5 seconds ignore this click 55 | clickedNPCCache.put(player, npc); 56 | 57 | NPCInteractionEvent event = new NPCInteractionEvent(npc, player, action); 58 | Bukkit.getPluginManager().callEvent(event); 59 | }, 2)); 60 | } 61 | 62 | public NPC newNPC(NPCOptions options) { 63 | ServerVersion serverVersion = NMSHelper.getInstance().getServerVersion(); 64 | NPC npc = null; 65 | 66 | if (useReflection) { 67 | serverVersion = ServerVersion.REFLECTED; 68 | } 69 | 70 | switch (serverVersion) { 71 | case REFLECTED: 72 | npc = new NPC_Reflection(plugin, options); 73 | break; 74 | case v1_16_R3: 75 | npc = new NPC_v1_16_R3(plugin, options); 76 | break; 77 | } 78 | 79 | if (npc == null) { 80 | throw new IllegalStateException("Invalid server version " + serverVersion + ". This plugin needs to be updated!"); 81 | } 82 | 83 | registeredNPCs.add(npc); 84 | return npc; 85 | } 86 | 87 | public Optional findNPC(String name) { 88 | return registeredNPCs.stream() 89 | .filter(npc -> npc.getName().equalsIgnoreCase(name)) 90 | .findFirst(); 91 | } 92 | 93 | public void deleteNPC(NPC npc) { 94 | npc.delete(); 95 | registeredNPCs.remove(npc); 96 | } 97 | 98 | public void deleteAllNPCs() { 99 | // Copy the set to prevent concurrent modification exception 100 | Set npcsCopy = new HashSet<>(registeredNPCs); 101 | npcsCopy.forEach(this::deleteNPC); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/NPCOptions.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.bukkit.Location; 6 | 7 | @Builder 8 | @Getter 9 | public class NPCOptions { 10 | private final String name; 11 | private final String texture; 12 | private final String signature; 13 | private final Location location; 14 | private final boolean hideNametag; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/ServerVersion.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs; 2 | 3 | public enum ServerVersion { 4 | v1_16_R3, 5 | REFLECTED, 6 | UNKNOWN; 7 | 8 | @Override 9 | public String toString() { 10 | return name(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/events/NPCClickAction.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.events; 2 | 3 | import com.comphenix.protocol.wrappers.EnumWrappers; 4 | 5 | public enum NPCClickAction { 6 | INTERACT, INTERACT_AT, ATTACK; 7 | 8 | public static NPCClickAction fromProtocolLibAction(EnumWrappers.EntityUseAction action) { 9 | switch (action) { 10 | case ATTACK: return ATTACK; 11 | case INTERACT: return INTERACT; 12 | case INTERACT_AT: return INTERACT_AT; 13 | default: return null; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/events/NPCInteractionEvent.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.events; 2 | 3 | import dev.jcsoftware.npcs.NPC; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Event; 7 | import org.bukkit.event.HandlerList; 8 | 9 | public class NPCInteractionEvent extends Event { 10 | private static final HandlerList HANDLER_LIST = new HandlerList(); 11 | 12 | @Getter 13 | private final NPC clicked; 14 | 15 | @Getter 16 | private final Player player; 17 | 18 | @Getter 19 | private final NPCClickAction clickAction; 20 | 21 | public NPCInteractionEvent(NPC clicked, Player player, NPCClickAction clickAction) { 22 | this.clicked = clicked; 23 | this.player = player; 24 | this.clickAction = clickAction; 25 | } 26 | 27 | @Override 28 | public HandlerList getHandlers() { 29 | return HANDLER_LIST; 30 | } 31 | 32 | public static HandlerList getHandlerList() { 33 | return HANDLER_LIST; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/example/NPCPlugin.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.example; 2 | 3 | import dev.jcsoftware.minecraft.gui.GUIAPI; 4 | import dev.jcsoftware.npcs.NPC; 5 | import dev.jcsoftware.npcs.NPCManager; 6 | import dev.jcsoftware.npcs.NPCOptions; 7 | import dev.jcsoftware.npcs.events.NPCClickAction; 8 | import dev.jcsoftware.npcs.events.NPCInteractionEvent; 9 | import dev.jcsoftware.npcs.example.gui.ServerMenuGUI; 10 | import org.bukkit.command.Command; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.Listener; 15 | import org.bukkit.plugin.java.JavaPlugin; 16 | 17 | public final class NPCPlugin extends JavaPlugin implements Listener { 18 | 19 | private NPCManager npcManager; 20 | private GUIAPI guiAPI; 21 | 22 | private final boolean USE_REFLECTION = false; 23 | 24 | @Override 25 | public void onEnable() { 26 | this.npcManager = new NPCManager(this, USE_REFLECTION); 27 | this.guiAPI = new GUIAPI<>(this); 28 | 29 | getServer().getPluginManager().registerEvents(this, this); 30 | } 31 | 32 | @Override 33 | public void onDisable() { 34 | npcManager.deleteAllNPCs(); 35 | } 36 | 37 | @Override 38 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 39 | if (!(sender instanceof Player)) return true; 40 | Player player = (Player) sender; 41 | 42 | if (command.getLabel().equalsIgnoreCase("spawn")) { 43 | NPC npc = npcManager.newNPC(NPCOptions.builder() 44 | .name("Technoblade") 45 | .hideNametag(false) 46 | // See https://sessionserver.mojang.com/session/minecraft/profile/b876ec32e396476ba1158438d83c67d4?unsigned=false 47 | // This will give Technoblade's Texture & Signature 48 | .texture("ewogICJ0aW1lc3RhbXAiIDogMTYxODM1Nzg1Mjc3NSwKICAicHJvZmlsZUlkIiA6ICJiODc2ZWMzMmUzOTY0NzZiYTExNTg0MzhkODNjNjdkNCIsCiAgInByb2ZpbGVOYW1lIiA6ICJUZWNobm9ibGFkZSIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS83ODZjMDM5ZDk2OWQxODM5MTU1MjU1ZTM4ZTdiMDZhNjI2ZWE5ZjhiYWY5Y2I1NWUwYTc3MzExZWZlMThhM2UiCiAgICB9CiAgfQp9") 49 | .signature("VYUQfmkBsHTXWf8tRRCiws/A/iwA+VIZ8wrbp4IdcM1CnYhZP+LTrVXSSl8bc88vQPbGxdL2Ks3Ow4cmBnGWe1ezpHWRO4vcyXRvh0AOle3XGYI31x7usryY9rr/37xLTdKqh7V7Ox4Dq9qt8Bmo8QBolpXBT6HlCbPPG6cu5AlycWTsoA6X0zvfWihLXH1suIU4LPeaORX1SpppzCGo1mz/SI0HaLM5vJIhktf8mJqP0DwUQetezb+b+LtJenoFp2lE/qRcrRF739NuwMw6tniea1dn3ftAWBH8l0r3p6uDzOAjJOxGnR5YBWfOewWF3x+k2UXkKqC01pPu1S8PbQDayP0++XsXw+28wvI/5G4U2otIoEU4lucViJPjWXmn2acE5LNq8eHaAm+5pBCmJ1TNGZkDlTHekivW1kaFh2NQCY3SyizUWjcPVE6aYZK8c2bltGOcKhgzJb7hYnjdbTX0S7KMD1csCN1bUduyv9byzvJkpVNka3LavCZCIPJ1ICpLFwQemdzaqXTp2x+5lnxKCMLu0EpDikX1Hcm86pJpW4qxXcZNRyCEwlulseIvRIgyfNzjDO2F8CYf94JqQVZ/pKonuRnJGTuWzur788JfaWcfrOv0hCUt8F5Yw1BCkBsucDhPaOwvQLPLET7+aPhuermXKsiw5UasB+OGhlA=") 50 | .location(player.getLocation()) 51 | .build()); 52 | npc.showTo(player); 53 | } 54 | 55 | return true; 56 | } 57 | 58 | @EventHandler 59 | private void onNPCClick(NPCInteractionEvent event) { 60 | NPC clicked = event.getClicked(); 61 | 62 | if (clicked.getName().equalsIgnoreCase("Technoblade")) { 63 | if (event.getClickAction() == NPCClickAction.ATTACK) return; 64 | guiAPI.openGUI(event.getPlayer(), new ServerMenuGUI(this)); 65 | } else { 66 | event.getPlayer().sendMessage("<" + clicked.getName() + "> Sorry, I don't think you're looking for me."); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/example/gui/ServerMenuGUI.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.example.gui; 2 | 3 | import dev.jcsoftware.minecraft.gui.GUI; 4 | import dev.jcsoftware.npcs.example.NPCPlugin; 5 | import org.bukkit.Material; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.inventory.ItemStack; 8 | 9 | public class ServerMenuGUI extends GUI { 10 | public ServerMenuGUI(NPCPlugin plugin) { 11 | super(plugin); 12 | 13 | set(11, new ItemStack(Material.GOLDEN_APPLE), (player, item) -> { 14 | player.sendMessage(" Looks good to me."); 15 | return ButtonAction.CLOSE_GUI; 16 | }); 17 | 18 | set(13, new ItemStack(Material.CARROT), (player, item) -> { 19 | player.sendMessage(" You animal..."); 20 | return ButtonAction.CLOSE_GUI; 21 | }); 22 | 23 | set(15, new ItemStack(Material.REDSTONE), (player, item) -> { 24 | player.sendMessage(" Blood for the blood god!"); 25 | return ButtonAction.CLOSE_GUI; 26 | }); 27 | } 28 | 29 | @Override 30 | public int getSize() { 31 | return 27; 32 | } 33 | 34 | @Override 35 | public String getTitle() { 36 | return "Server Menu"; 37 | } 38 | 39 | @Override 40 | public boolean canClose(Player player) { 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/utility/StringUtility.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.utility; 2 | 3 | import java.util.Random; 4 | 5 | public class StringUtility { 6 | public static String randomCharacters(int length) { 7 | if (length < 1) { 8 | throw new IllegalArgumentException("Invalid length. Length must be at least 1 characters"); 9 | } 10 | 11 | int leftLimit = 97; // letter 'a' 12 | int rightLimit = 122; // letter 'z' 13 | Random random = new Random(); 14 | StringBuilder buffer = new StringBuilder(length); 15 | for (int i = 0; i < length; i++) { 16 | int randomLimitedInt = leftLimit + (int) 17 | (random.nextFloat() * (rightLimit - leftLimit + 1)); 18 | buffer.append((char) randomLimitedInt); 19 | } 20 | return buffer.toString(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/versioned/NPC_Reflection.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.versioned; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import com.mojang.authlib.properties.Property; 5 | import dev.jcsoftware.npcs.NMSHelper; 6 | import dev.jcsoftware.npcs.NPC; 7 | import dev.jcsoftware.npcs.NPCOptions; 8 | import dev.jcsoftware.npcs.utility.StringUtility; 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.ChatColor; 11 | import org.bukkit.Location; 12 | import org.bukkit.World; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | 16 | import java.lang.reflect.Array; 17 | import java.lang.reflect.Constructor; 18 | import java.lang.reflect.Method; 19 | import java.util.*; 20 | import java.util.stream.Collectors; 21 | 22 | public class NPC_Reflection implements NPC { 23 | private final JavaPlugin plugin; 24 | 25 | private final UUID uuid = UUID.randomUUID(); 26 | private final String name; 27 | private final String entityName; 28 | private final String texture; 29 | private final String signature; 30 | private final boolean hideNametag; 31 | 32 | private Object entityPlayer; 33 | 34 | public NPC_Reflection(JavaPlugin plugin, NPCOptions npcOptions) { 35 | this.plugin = plugin; 36 | 37 | this.name = npcOptions.getName(); 38 | this.texture = npcOptions.getTexture(); 39 | this.signature = npcOptions.getSignature(); 40 | this.hideNametag = npcOptions.isHideNametag(); 41 | 42 | if (hideNametag) { 43 | this.entityName = StringUtility.randomCharacters(10); 44 | } else { 45 | this.entityName = this.name; 46 | } 47 | 48 | addToWorld(npcOptions.getLocation()); 49 | } 50 | 51 | private void addToWorld(Location location) { 52 | try { 53 | NMSHelper nmsHelper = NMSHelper.getInstance(); 54 | 55 | Object minecraftServer = nmsHelper.getCraftBukkitClass("CraftServer").getMethod("getServer").invoke(Bukkit.getServer()); 56 | Object worldServer = nmsHelper.getCraftBukkitClass("CraftWorld").getMethod("getHandle").invoke(location.getWorld()); 57 | 58 | GameProfile gameProfile = makeGameProfile(); 59 | 60 | Constructor entityPlayerConstructor = nmsHelper.getNMSClass("EntityPlayer").getDeclaredConstructors()[0]; 61 | Constructor interactManagerConstructor = nmsHelper.getNMSClass("PlayerInteractManager").getDeclaredConstructors()[0]; 62 | Object interactManager = interactManagerConstructor.newInstance(worldServer); 63 | 64 | this.entityPlayer = entityPlayerConstructor.newInstance(minecraftServer, worldServer, gameProfile, interactManager); 65 | 66 | this.entityPlayer.getClass().getMethod("setLocation", double.class, double.class, double.class, float.class, float.class).invoke(entityPlayer, 67 | location.getX(), 68 | location.getY(), 69 | location.getZ(), 70 | location.getYaw(), 71 | location.getPitch()); 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | private GameProfile makeGameProfile() { 78 | GameProfile gameProfile = new GameProfile(uuid, entityName); 79 | gameProfile.getProperties().put( 80 | "textures", 81 | new Property("textures", 82 | texture, 83 | signature 84 | ) 85 | ); 86 | return gameProfile; 87 | } 88 | 89 | @Override 90 | public String getName() { 91 | return name; 92 | } 93 | 94 | private final Set viewers = new HashSet<>(); 95 | 96 | @Override 97 | public void showTo(Player player) { 98 | try { 99 | NMSHelper nmsHelper = NMSHelper.getInstance(); 100 | 101 | viewers.add(player.getUniqueId()); 102 | 103 | // PacketPlayOutPlayerInfo 104 | Object addPlayerEnum = nmsHelper.getNMSClass("PacketPlayOutPlayerInfo$EnumPlayerInfoAction").getField("ADD_PLAYER").get(null); 105 | Constructor packetPlayOutPlayerInfoConstructor = nmsHelper.getNMSClass("PacketPlayOutPlayerInfo") 106 | .getConstructor( 107 | nmsHelper.getNMSClass("PacketPlayOutPlayerInfo$EnumPlayerInfoAction"), 108 | Class.forName("[Lnet.minecraft.server." + nmsHelper.getServerVersionString() + ".EntityPlayer;") 109 | ); 110 | 111 | Object array = Array.newInstance(nmsHelper.getNMSClass("EntityPlayer"), 1); 112 | Array.set(array, 0, this.entityPlayer); 113 | 114 | Object packetPlayOutPlayerInfo = packetPlayOutPlayerInfoConstructor.newInstance(addPlayerEnum, array); 115 | sendPacket(player, packetPlayOutPlayerInfo); 116 | 117 | // PacketPlayOutNamedEntitySpawn 118 | Constructor packetPlayOutNamedEntitySpawnConstructor = nmsHelper.getNMSClass("PacketPlayOutNamedEntitySpawn") 119 | .getConstructor(nmsHelper.getNMSClass("EntityHuman")); 120 | Object packetPlayOutNamedEntitySpawn = packetPlayOutNamedEntitySpawnConstructor.newInstance(this.entityPlayer); 121 | sendPacket(player, packetPlayOutNamedEntitySpawn); 122 | 123 | // Scoreboard Team 124 | Object scoreboardManager = Bukkit.getServer().getClass().getMethod("getScoreboardManager") 125 | .invoke(Bukkit.getServer()); 126 | Object mainScoreboard = scoreboardManager.getClass().getMethod("getMainScoreboard") 127 | .invoke(scoreboardManager); 128 | Object scoreboard = mainScoreboard.getClass().getMethod("getHandle").invoke(mainScoreboard); 129 | 130 | Method getTeamMethod = scoreboard.getClass().getMethod("getTeam", String.class); 131 | Constructor scoreboardTeamConstructor = nmsHelper.getNMSClass("ScoreboardTeam").getDeclaredConstructor(nmsHelper.getNMSClass("Scoreboard"), String.class); 132 | 133 | Object scoreboardTeam = getTeamMethod.invoke(scoreboard, entityName) == null ? 134 | scoreboardTeamConstructor.newInstance(scoreboard, entityName) : 135 | getTeamMethod.invoke(scoreboard, entityName); 136 | 137 | Class nameTagStatusEnum = nmsHelper.getNMSClass("ScoreboardTeamBase$EnumNameTagVisibility"); 138 | Method setNameTagVisibility = scoreboardTeam.getClass().getMethod("setNameTagVisibility", nameTagStatusEnum); 139 | 140 | if (hideNametag) { 141 | setNameTagVisibility.invoke(scoreboardTeam, nameTagStatusEnum.getField("NEVER").get(null)); 142 | } else { 143 | setNameTagVisibility.invoke(scoreboardTeam, nameTagStatusEnum.getField("ALWAYS").get(null)); 144 | } 145 | 146 | Class collisionStatusEnum = nmsHelper.getNMSClass("ScoreboardTeamBase$EnumTeamPush"); 147 | Method setCollisionRule = scoreboardTeam.getClass().getMethod("setCollisionRule", collisionStatusEnum); 148 | 149 | setCollisionRule.invoke(scoreboardTeam, collisionStatusEnum.getField("NEVER").get(null)); 150 | 151 | if (hideNametag) { 152 | Object grayChatFormat = nmsHelper.getNMSClass("EnumChatFormat").getField("GRAY").get(null); 153 | scoreboardTeam.getClass().getMethod("setColor", nmsHelper.getNMSClass("EnumChatFormat")) 154 | .invoke(scoreboardTeam, grayChatFormat); 155 | 156 | Constructor chatMessageConstructor = nmsHelper.getNMSClass("ChatMessage").getDeclaredConstructor(String.class); 157 | scoreboardTeam.getClass().getMethod("setPrefix", nmsHelper.getNMSClass("IChatBaseComponent")) 158 | .invoke(scoreboardTeam, chatMessageConstructor.newInstance(ChatColor.COLOR_CHAR + "7[NPC] ")); 159 | } 160 | 161 | Class packetPlayOutScoreboardTeamClass = nmsHelper.getNMSClass("PacketPlayOutScoreboardTeam"); 162 | Constructor packetPlayOutScoreboardTeamTeamIntConstructor = packetPlayOutScoreboardTeamClass.getConstructor(nmsHelper.getNMSClass("ScoreboardTeam"), int.class); 163 | Constructor packetPlayOutScoreboardTeamTeamCollectionIntConstructor = packetPlayOutScoreboardTeamClass.getConstructor(nmsHelper.getNMSClass("ScoreboardTeam"), Collection.class, int.class); 164 | 165 | sendPacket(player, packetPlayOutScoreboardTeamTeamIntConstructor.newInstance(scoreboardTeam, 1)); 166 | sendPacket(player, packetPlayOutScoreboardTeamTeamIntConstructor.newInstance(scoreboardTeam, 0)); 167 | sendPacket( 168 | player, 169 | packetPlayOutScoreboardTeamTeamCollectionIntConstructor.newInstance( 170 | scoreboardTeam, 171 | Collections.singletonList(entityName), 172 | 3 173 | ) 174 | ); 175 | 176 | sendHeadRotationPacket(player); 177 | 178 | Bukkit.getServer().getScheduler().runTaskTimer(plugin, task -> { 179 | Player currentlyOnline = Bukkit.getPlayer(player.getUniqueId()); 180 | if (currentlyOnline == null || !currentlyOnline.isOnline()) { 181 | task.cancel(); 182 | return; 183 | } 184 | 185 | sendHeadRotationPacket(player); 186 | }, 0, 2); 187 | 188 | Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> { 189 | try { 190 | Object removePlayerEnum = nmsHelper.getNMSClass("PacketPlayOutPlayerInfo$EnumPlayerInfoAction").getField("REMOVE_PLAYER").get(null); 191 | Object removeFromTabPacket = packetPlayOutPlayerInfoConstructor.newInstance(removePlayerEnum, array); 192 | sendPacket(player, removeFromTabPacket); 193 | } catch (Exception exception) { 194 | exception.printStackTrace(); 195 | } 196 | }, 20); 197 | 198 | Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> { 199 | fixSkinHelmetLayerForPlayer(player); 200 | }, 8); 201 | } catch (Exception e) { 202 | e.printStackTrace(); 203 | } 204 | } 205 | 206 | @Override 207 | public void hideFrom(Player player) { 208 | if (!viewers.contains(player.getUniqueId())) return; 209 | viewers.remove(player.getUniqueId()); 210 | 211 | try { 212 | Constructor destroyPacketConstructor = NMSHelper.getInstance() 213 | .getNMSClass("PacketPlayOutEntityDestroy").getConstructor(int[].class); 214 | Object packet = destroyPacketConstructor.newInstance((Object) new int[]{getId()}); 215 | sendPacket(player, packet); 216 | } catch (Exception e) { 217 | e.printStackTrace(); 218 | } 219 | } 220 | 221 | @Override 222 | public void delete() { 223 | Set onlineViewers = viewers.stream() 224 | .map(Bukkit::getPlayer) 225 | .filter(Objects::nonNull) 226 | .collect(Collectors.toSet()); 227 | onlineViewers.forEach(this::hideFrom); 228 | } 229 | 230 | private void sendHeadRotationPacket(Player player) { 231 | NMSHelper nmsHelper = NMSHelper.getInstance(); 232 | 233 | Location original = getLocation(); 234 | Location location = original.clone().setDirection(player.getLocation().subtract(original.clone()).toVector()); 235 | 236 | byte yaw = (byte) (location.getYaw() * 256 / 360); 237 | byte pitch = (byte) (location.getPitch() * 256 / 360); 238 | 239 | try { 240 | // PacketPlayOutEntityHeadRotation 241 | Constructor packetPlayOutEntityHeadRotationConstructor = nmsHelper.getNMSClass("PacketPlayOutEntityHeadRotation") 242 | .getConstructor(nmsHelper.getNMSClass("Entity"), byte.class); 243 | 244 | Object packetPlayOutEntityHeadRotation = packetPlayOutEntityHeadRotationConstructor.newInstance(this.entityPlayer, yaw); 245 | sendPacket(player, packetPlayOutEntityHeadRotation); 246 | 247 | Constructor packetPlayOutEntityLookConstructor = nmsHelper.getNMSClass("PacketPlayOutEntity$PacketPlayOutEntityLook") 248 | .getConstructor(int.class, byte.class, byte.class, boolean.class); 249 | Object packetPlayOutEntityLook = packetPlayOutEntityLookConstructor.newInstance(getId(), yaw, pitch, false); 250 | sendPacket(player, packetPlayOutEntityLook); 251 | } catch (Exception e) { 252 | e.printStackTrace(); 253 | } 254 | } 255 | 256 | public void fixSkinHelmetLayerForPlayer(Player player) { 257 | Byte skinFixByte = 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40; 258 | sendMetadata(player, 16, skinFixByte); 259 | } 260 | 261 | private void sendMetadata(Player player, int index, Byte o) { 262 | NMSHelper nmsHelper = NMSHelper.getInstance(); 263 | 264 | try { 265 | Object dataWatcher = entityPlayer.getClass().getMethod("getDataWatcher").invoke(entityPlayer); 266 | Class dataWatcherRegistryClass = nmsHelper.getNMSClass("DataWatcherRegistry"); 267 | Object registry = dataWatcherRegistryClass.getField("a").get(null); 268 | Method watcherCreateMethod = registry.getClass().getMethod("a", int.class); 269 | 270 | Method dataWatcherSetMethod = dataWatcher.getClass().getMethod("set", nmsHelper.getNMSClass("DataWatcherObject"), Object.class); 271 | dataWatcherSetMethod.invoke(dataWatcher, watcherCreateMethod.invoke(registry, index), o); 272 | 273 | Constructor packetPlayOutEntityMetadataConstructor = nmsHelper.getNMSClass("PacketPlayOutEntityMetadata") 274 | .getDeclaredConstructor(int.class, nmsHelper.getNMSClass("DataWatcher"), boolean.class); 275 | sendPacket(player, packetPlayOutEntityMetadataConstructor.newInstance(getId(), dataWatcher, false)); 276 | } catch (Exception e) { 277 | e.printStackTrace(); 278 | } 279 | } 280 | 281 | @Override 282 | public Location getLocation() { 283 | try { 284 | Class EntityPlayer = entityPlayer.getClass(); 285 | 286 | Object minecraftWorld = EntityPlayer.getMethod("getWorld").invoke(entityPlayer); 287 | Object craftWorld = minecraftWorld.getClass().getMethod("getWorld").invoke(minecraftWorld); 288 | 289 | double locX = (double) EntityPlayer.getMethod("locX").invoke(entityPlayer); 290 | double locY = (double) EntityPlayer.getMethod("locY").invoke(entityPlayer); 291 | double locZ = (double) EntityPlayer.getMethod("locZ").invoke(entityPlayer); 292 | float yaw = (float) EntityPlayer.getField("yaw").get(entityPlayer); 293 | float pitch = (float) EntityPlayer.getField("pitch").get(entityPlayer); 294 | 295 | return new Location( 296 | (World) craftWorld, 297 | locX, 298 | locY, 299 | locZ, 300 | yaw, 301 | pitch 302 | ); 303 | } catch (Exception e) { 304 | e.printStackTrace(); 305 | return null; 306 | } 307 | } 308 | 309 | @Override 310 | public int getId() { 311 | if (entityPlayer == null) return -1; 312 | 313 | try { 314 | Method getId = entityPlayer.getClass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("getId"); 315 | return (int) getId.invoke(entityPlayer); 316 | } catch (Exception e) { 317 | e.printStackTrace(); 318 | return -1; 319 | } 320 | } 321 | 322 | private void sendPacket(Player player, Object packet) { 323 | NMSHelper.getInstance().sendPacket(player, packet); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/java/dev/jcsoftware/npcs/versioned/NPC_v1_16_R3.java: -------------------------------------------------------------------------------- 1 | package dev.jcsoftware.npcs.versioned; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import com.mojang.authlib.properties.Property; 5 | import dev.jcsoftware.npcs.NMSHelper; 6 | import dev.jcsoftware.npcs.NPCOptions; 7 | import dev.jcsoftware.npcs.utility.StringUtility; 8 | import lombok.SneakyThrows; 9 | import net.minecraft.server.v1_16_R3.*; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.ChatColor; 12 | import org.bukkit.Location; 13 | import org.bukkit.craftbukkit.v1_16_R3.CraftServer; 14 | import org.bukkit.craftbukkit.v1_16_R3.CraftWorld; 15 | import org.bukkit.craftbukkit.v1_16_R3.scoreboard.CraftScoreboard; 16 | import org.bukkit.craftbukkit.v1_16_R3.scoreboard.CraftScoreboardManager; 17 | import org.bukkit.entity.Player; 18 | import org.bukkit.plugin.java.JavaPlugin; 19 | 20 | import java.util.*; 21 | import java.util.stream.Collectors; 22 | 23 | public class NPC_v1_16_R3 implements dev.jcsoftware.npcs.NPC { 24 | private final JavaPlugin plugin; 25 | 26 | private final UUID uuid = UUID.randomUUID(); 27 | private final String name; 28 | private final String entityName; 29 | private final String texture; 30 | private final String signature; 31 | private final boolean hideNametag; 32 | 33 | private EntityPlayer entityPlayer; 34 | 35 | public NPC_v1_16_R3(JavaPlugin plugin, NPCOptions npcOptions) { 36 | this.plugin = plugin; 37 | 38 | this.name = npcOptions.getName(); 39 | this.texture = npcOptions.getTexture(); 40 | this.signature = npcOptions.getSignature(); 41 | this.hideNametag = npcOptions.isHideNametag(); 42 | 43 | if (hideNametag) { 44 | this.entityName = StringUtility.randomCharacters(10); 45 | } else { 46 | this.entityName = this.name; 47 | } 48 | 49 | addToWorld(npcOptions.getLocation()); 50 | } 51 | 52 | private void addToWorld(Location location) { 53 | if (location.getWorld() == null) { 54 | throw new IllegalArgumentException("NPC Location world cannot be null"); 55 | } 56 | 57 | MinecraftServer minecraftServer = ((CraftServer) Bukkit.getServer()).getServer(); 58 | WorldServer worldServer = ((CraftWorld) location.getWorld()).getHandle(); 59 | GameProfile gameProfile = makeGameProfile(); 60 | 61 | PlayerInteractManager interactManager = new PlayerInteractManager(worldServer); 62 | entityPlayer = new EntityPlayer( 63 | minecraftServer, 64 | worldServer, 65 | gameProfile, 66 | interactManager 67 | ); 68 | 69 | entityPlayer.setLocation( 70 | location.getX(), 71 | location.getY(), 72 | location.getZ(), 73 | location.getYaw(), 74 | location.getPitch() 75 | ); 76 | } 77 | 78 | private GameProfile makeGameProfile() { 79 | GameProfile gameProfile = new GameProfile(uuid, entityName); 80 | gameProfile.getProperties().put( 81 | "textures", 82 | new Property("textures", 83 | texture, 84 | signature 85 | ) 86 | ); 87 | return gameProfile; 88 | } 89 | 90 | private final Set viewers = new HashSet<>(); 91 | public void showTo(Player player) { 92 | viewers.add(player.getUniqueId()); 93 | 94 | PacketPlayOutPlayerInfo packetPlayOutPlayerInfo = new PacketPlayOutPlayerInfo( 95 | PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, 96 | entityPlayer 97 | ); 98 | sendPacket(player, packetPlayOutPlayerInfo); 99 | 100 | PacketPlayOutNamedEntitySpawn packetPlayOutNamedEntitySpawn = new PacketPlayOutNamedEntitySpawn( 101 | entityPlayer 102 | ); 103 | sendPacket(player, packetPlayOutNamedEntitySpawn); 104 | 105 | CraftScoreboardManager scoreboardManager = ((CraftServer) Bukkit.getServer()).getScoreboardManager(); 106 | assert scoreboardManager != null; 107 | CraftScoreboard mainScoreboard = scoreboardManager.getMainScoreboard(); 108 | Scoreboard scoreboard = mainScoreboard.getHandle(); 109 | 110 | ScoreboardTeam scoreboardTeam = scoreboard.getTeam(entityName); 111 | if (scoreboardTeam == null) { 112 | scoreboardTeam = new ScoreboardTeam(scoreboard, entityName); 113 | } 114 | 115 | scoreboardTeam.setNameTagVisibility( 116 | hideNametag ? ScoreboardTeamBase.EnumNameTagVisibility.NEVER : ScoreboardTeamBase.EnumNameTagVisibility.ALWAYS 117 | ); 118 | scoreboardTeam.setCollisionRule(ScoreboardTeamBase.EnumTeamPush.NEVER); 119 | 120 | if (hideNametag) { 121 | scoreboardTeam.setColor(EnumChatFormat.GRAY); 122 | scoreboardTeam.setPrefix( 123 | new ChatMessage(ChatColor.COLOR_CHAR + "7[NPC] ") 124 | ); 125 | } 126 | 127 | sendPacket(player, new PacketPlayOutScoreboardTeam(scoreboardTeam, 1)); // Create team 128 | sendPacket(player, new PacketPlayOutScoreboardTeam(scoreboardTeam, 0)); // Setup team options 129 | sendPacket(player, new PacketPlayOutScoreboardTeam(scoreboardTeam, Collections.singletonList(entityName), 3)); // Add entityPlayer to team entries 130 | 131 | Bukkit.getServer().getScheduler().runTaskTimer(plugin, task -> { 132 | Player currentlyOnline = Bukkit.getPlayer(player.getUniqueId()); 133 | if (currentlyOnline == null || 134 | !currentlyOnline.isOnline() || 135 | !viewers.contains(player.getUniqueId())) { 136 | task.cancel(); 137 | return; 138 | } 139 | 140 | sendHeadRotationPacket(player); 141 | }, 0, 2); 142 | 143 | Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> { 144 | try { 145 | PacketPlayOutPlayerInfo removeFromTabPacket = new PacketPlayOutPlayerInfo( 146 | PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, 147 | entityPlayer 148 | ); 149 | sendPacket(player, removeFromTabPacket); 150 | } catch (Exception ex) { 151 | ex.printStackTrace(); 152 | } 153 | }, 20); 154 | 155 | Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> { 156 | fixSkinHelmetLayerForPlayer(player); 157 | }, 8); 158 | } 159 | 160 | public void hideFrom(Player player) { 161 | if (!viewers.contains(player.getUniqueId())) return; 162 | viewers.remove(player.getUniqueId()); 163 | 164 | PacketPlayOutEntityDestroy packet = new PacketPlayOutEntityDestroy(entityPlayer.getId()); 165 | sendPacket(player, packet); 166 | } 167 | 168 | @Override 169 | public void delete() { 170 | Set onlineViewers = viewers.stream() 171 | .map(Bukkit::getPlayer) 172 | .filter(Objects::nonNull) 173 | .collect(Collectors.toSet()); 174 | onlineViewers.forEach(this::hideFrom); 175 | } 176 | 177 | @SneakyThrows 178 | public void sendHeadRotationPacket(Player player) { 179 | Location original = getLocation(); 180 | Location location = original.clone().setDirection(player.getLocation().subtract(original.clone()).toVector()); 181 | 182 | byte yaw = (byte) (location.getYaw() * 256 / 360); 183 | byte pitch = (byte) (location.getPitch() * 256 / 360); 184 | 185 | PacketPlayOutEntityHeadRotation headRotationPacket = new PacketPlayOutEntityHeadRotation( 186 | this.entityPlayer, 187 | yaw 188 | ); 189 | sendPacket(player, headRotationPacket); 190 | 191 | PacketPlayOutEntity.PacketPlayOutEntityLook lookPacket = new PacketPlayOutEntity.PacketPlayOutEntityLook( 192 | getId(), 193 | yaw, 194 | pitch, 195 | false 196 | ); 197 | sendPacket(player, lookPacket); 198 | } 199 | 200 | public void fixSkinHelmetLayerForPlayer(Player player) { 201 | Byte skinFixByte = 0x01 | 0x02 | 0x04 | 0x08 | 0x10 | 0x20 | 0x40; 202 | sendMetadata(player, 16, skinFixByte); 203 | } 204 | 205 | @SneakyThrows 206 | private void sendMetadata(Player player, int index, Byte o) { 207 | DataWatcher dataWatcher = entityPlayer.getDataWatcher(); 208 | 209 | DataWatcherSerializer registry = DataWatcherRegistry.a; 210 | dataWatcher.set( 211 | registry.a(index), 212 | o 213 | ); 214 | 215 | PacketPlayOutEntityMetadata metadataPacket = new PacketPlayOutEntityMetadata( 216 | getId(), 217 | dataWatcher, 218 | false 219 | ); 220 | sendPacket(player, metadataPacket); 221 | } 222 | 223 | public Location getLocation() { 224 | return new Location( 225 | entityPlayer.getWorld().getWorld(), 226 | entityPlayer.locX(), 227 | entityPlayer.locY(), 228 | entityPlayer.locZ(), 229 | entityPlayer.yaw, 230 | entityPlayer.pitch 231 | ); 232 | } 233 | 234 | public int getId() { 235 | return entityPlayer.getId(); 236 | } 237 | 238 | @Override 239 | public String getName() { 240 | return name; 241 | } 242 | 243 | private void sendPacket(Player player, Object packet) { 244 | NMSHelper.getInstance().sendPacket(player, packet); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: NPCPlugin 2 | version: ${project.version} 3 | main: dev.jcsoftware.npcs.example.NPCPlugin 4 | api-version: 1.16 5 | authors: 6 | - Jordan Osterberg 7 | - cococow123 8 | depend: 9 | - ProtocolLib 10 | commands: 11 | spawn: 12 | description: Spawn an NPC 13 | --------------------------------------------------------------------------------