├── .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 |
--------------------------------------------------------------------------------