/* * Copyright (c) 2018 Jitse Boonstra */ package net.jitse.npclib.internal; import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.Property; import net.jitse.npclib.NPCLib; import net.jitse.npclib.api.NPC; import net.jitse.npclib.api.events.NPCHideEvent; import net.jitse.npclib.api.events.NPCShowEvent; import net.jitse.npclib.api.skin.Skin; import net.jitse.npclib.api.state.NPCSlot; import net.jitse.npclib.api.state.NPCState; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; import java.util.*; public abstract class SimpleNPC implements NPC, PacketHandler { protected final UUID uuid = UUID.randomUUID(); protected final int entityId = Integer.MAX_VALUE - NPCManager.getAllNPCs().size(); protected final String name = uuid.toString().replace("-", "").substring(0, 10); protected final GameProfile gameProfile = new GameProfile(uuid, name); protected final List lines; private final Set shown = new HashSet<>(); private final Set autoHidden = new HashSet<>(); protected double cosFOV = Math.cos(Math.toRadians(60)); protected NPCState[] activeStates = new NPCState[]{}; protected NPCLib instance; protected Location location; protected Skin skin; // offHand support in 1.9 R1 and later. protected ItemStack helmet, chestplate, leggings, boots, inHand, offHand; public SimpleNPC(NPCLib instance, List lines) { this.instance = instance; this.lines = lines == null ? Collections.emptyList() : lines; NPCManager.add(this); } public NPCLib getInstance() { return instance; } @Override public String getId() { return name; } @Override public NPC setSkin(Skin skin) { this.skin = skin; gameProfile.getProperties().get("textures").clear(); if (skin != null) gameProfile.getProperties().put("textures", new Property("textures", skin.getValue(), skin.getSignature())); return this; } @Override public void destroy() { NPCManager.remove(this); // Destroy NPC for every player that is still seeing it. for (UUID uuid : shown) { if (autoHidden.contains(uuid)) { continue; } hide(Bukkit.getPlayer(uuid), true); } } public void disableFOV() { this.cosFOV = 0; // Or equals Math.cos(1/2 * Math.PI). } public void setFOV(double fov) { this.cosFOV = Math.cos(Math.toRadians(fov)); } public Set getShown() { return shown; } public Set getAutoHidden() { return autoHidden; } @Override public Location getLocation() { return location; } @Override public World getWorld() { return location != null ? location.getWorld() : null; } public int getEntityId() { return entityId; } @Override public boolean isShown(Player player) { return shown.contains(player.getUniqueId()) && !autoHidden.contains(player.getUniqueId()); } @Override public NPC setLocation(Location location) { this.location = location; return this; } @Override public NPC create() { createPackets(); return this; } public void onLogout(Player player) { getAutoHidden().remove(player.getUniqueId()); getShown().remove(player.getUniqueId()); // Don't need to use NPC#hide since the entity is not registered in the NMS server. } @Override public void show(Player player) { show(player, false); } public void show(Player player, boolean auto) { NPCShowEvent event = new NPCShowEvent(this, player, auto); Bukkit.getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { return; } if (!canSeeNPC(player)) { if (!auto) { shown.add(player.getUniqueId()); } autoHidden.add(player.getUniqueId()); return; } if (auto) { sendShowPackets(player); sendMetadataPacket(player); sendEquipmentPackets(player); } else { if (isShown(player)) { throw new RuntimeException("Cannot call show method twice."); } if (shown.contains(player.getUniqueId())) { return; } shown.add(player.getUniqueId()); if (player.getWorld().equals(location.getWorld()) && player.getLocation().distance(location) <= instance.getAutoHideDistance()) { sendShowPackets(player); sendMetadataPacket(player); sendEquipmentPackets(player); } else { autoHidden.add(player.getUniqueId()); } } } private boolean canSeeNPC(Player player) { Vector dir = location.toVector().subtract(player.getEyeLocation().toVector()).normalize(); return dir.dot(player.getLocation().getDirection()) >= cosFOV; } @Override public void hide(Player player) { hide(player, false); } public void hide(Player player, boolean auto) { NPCHideEvent event = new NPCHideEvent(this, player, auto); Bukkit.getServer().getPluginManager().callEvent(event); if (event.isCancelled()) { return; } if (auto) { sendHidePackets(player); } else { if (!shown.contains(player.getUniqueId())) { throw new RuntimeException("Cannot call hide method without calling NPC#show."); } shown.remove(player.getUniqueId()); if (player.getWorld().equals(location.getWorld()) && player.getLocation().distance(location) <= instance.getAutoHideDistance()) { sendHidePackets(player); } else { autoHidden.remove(player.getUniqueId()); } } } @Override public NPC toggleState(NPCState state) { int inActiveStatesIndex = -1; if (activeStates.length == 0) { // If there're no active states, this is the first to be toggled (on). activeStates = new NPCState[]{state}; } else { // Otherwise, there have been states that were toggled, check if we need to toggle something off. for (int i = 0; i < activeStates.length; i++) { if (activeStates[i] == state) { // If the state is to be toggled off, save the index so we can remove it. inActiveStatesIndex = i; break; } } if (inActiveStatesIndex > -1) { // If there's a state to be toggled of, create a new array with all items but the one to be toggled off. NPCState[] newArr = new NPCState[activeStates.length - 1]; for (int i = 0; i < newArr.length; i++) { if (inActiveStatesIndex == i) { continue; } else if (i < inActiveStatesIndex) { newArr[i] = activeStates[i]; } else { newArr[i] = activeStates[i + 1]; } } activeStates = newArr; } else { // Else, we need to add a state by appending our state to the array. NPCState[] newArr = new NPCState[activeStates.length + 1]; System.arraycopy(activeStates, 0, newArr, 0, activeStates.length); newArr[activeStates.length] = state; activeStates = newArr; } } // Send a new metadata packet to all players that can see the NPC. for (UUID shownUuid : shown) { Player player = Bukkit.getPlayer(shownUuid); if (player != null && isShown(player)) { sendMetadataPacket(player); } } return this; } @Override public NPC setItem(NPCSlot slot, ItemStack item) { if (slot == null) { throw new NullPointerException("Slot cannot be null"); } switch (slot) { case HELMET: this.helmet = item; break; case CHESTPLATE: this.chestplate = item; break; case LEGGINGS: this.leggings = item; break; case BOOTS: this.boots = item; break; case MAINHAND: this.inHand = item; break; case OFFHAND: this.offHand = item; break; default: throw new IllegalArgumentException("Entered an invalid inventory slot"); } for (UUID shownUuid : shown) { Player player = Bukkit.getPlayer(shownUuid); if (player != null && isShown(player)) { sendEquipmentPacket(player, slot, false); } } return this; } }